/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core */ 'use strict'; let React; let ReactDOM; let ReactDOMClient; let ReactDOMServer; let act; let assertConsoleErrorDev; describe('ReactComponent', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); act = require('internal-test-utils').act; assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; }); // @gate !disableLegacyMode it('should throw on invalid render targets in legacy roots', () => { const container = document.createElement('div'); // jQuery objects are basically arrays; people often pass them in by mistake expect(function () { ReactDOM.render(
, [container]); }).toThrowError(/Target container is not a DOM element./); expect(function () { ReactDOM.render(
, null); }).toThrowError(/Target container is not a DOM element./); }); // @gate !disableStringRefs it('should throw when supplying a string ref outside of render method', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( act(() => { root.render(
); }), // TODO: This throws an AggregateError. Need to update test infra to // support matching against AggregateError. ).rejects.toThrow(); }); it('should throw (in dev) when children are mutated during render', async () => { function Wrapper(props) { props.children[1] =

; // Mutation is illegal return

{props.children}
; } if (__DEV__) { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( act(() => { root.render( , ); }), ).rejects.toThrowError(/Cannot assign to read only property.*/); } else { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render( , ); }); } }); it('should throw (in dev) when children are mutated during update', async () => { class Wrapper extends React.Component { componentDidMount() { this.props.children[1] =

; // Mutation is illegal this.forceUpdate(); } render() { return

{this.props.children}
; } } if (__DEV__) { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( act(() => { root.render( , ); }), ).rejects.toThrowError(/Cannot assign to read only property.*/); } else { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render( , ); }); } }); // @gate !disableStringRefs it('string refs do not detach and reattach on every render', async () => { let refVal; class Child extends React.Component { componentDidUpdate() { // The parent ref should still be attached because it hasn't changed // since the last render. If the ref had changed, then this would be // undefined because refs are attached during the same phase (layout) // as componentDidUpdate, in child -> parent order. So the new parent // ref wouldn't have attached yet. refVal = this.props.contextRef(); } render() { if (this.props.show) { return
child
; } } } class Parent extends React.Component { render() { return (
this.refs.root} show={this.props.showChild} />
); } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); assertConsoleErrorDev(['contains the string ref']); expect(refVal).toBe(undefined); await act(() => { root.render(); }); expect(refVal).toBe(container.querySelector('#test-root')); }); // @gate !disableStringRefs it('should support string refs on owned components', async () => { const innerObj = {}; const outerObj = {}; class Wrapper extends React.Component { getObject = () => { return this.props.object; }; render() { return
{this.props.children}
; } } class Component extends React.Component { render() { const inner = ; const outer = ( {inner} ); return outer; } componentDidMount() { expect(this.refs.inner.getObject()).toEqual(innerObj); expect(this.refs.outer.getObject()).toEqual(outerObj); } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect(async () => { await act(() => { root.render(); }); }).toErrorDev([ 'Component "Component" contains the string ref "inner". ' + 'Support for string refs will be removed in a future major release. ' + 'We recommend using useRef() or createRef() instead. ' + 'Learn more about using refs safely here: https://react.dev/link/strict-mode-string-ref\n' + ' in Wrapper (at **)\n' + ' in div (at **)\n' + ' in Wrapper (at **)\n' + ' in Component (at **)', ]); }); it('should not have string refs on unmounted components', async () => { class Parent extends React.Component { render() { return (
); } componentDidMount() { expect(this.refs && this.refs.test).toEqual(undefined); } } class Child extends React.Component { render() { return
; } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(} />); }); }); it('should support callback-style refs', async () => { const innerObj = {}; const outerObj = {}; class Wrapper extends React.Component { getObject = () => { return this.props.object; }; render() { return
{this.props.children}
; } } let mounted = false; class Component extends React.Component { render() { const inner = ( (this.innerRef = c)} /> ); const outer = ( (this.outerRef = c)}> {inner} ); return outer; } componentDidMount() { expect(this.innerRef.getObject()).toEqual(innerObj); expect(this.outerRef.getObject()).toEqual(outerObj); mounted = true; } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(mounted).toBe(true); }); it('should support object-style refs', async () => { const innerObj = {}; const outerObj = {}; class Wrapper extends React.Component { getObject = () => { return this.props.object; }; render() { return
{this.props.children}
; } } let mounted = false; class Component extends React.Component { constructor() { super(); this.innerRef = React.createRef(); this.outerRef = React.createRef(); } render() { const inner = ; const outer = ( {inner} ); return outer; } componentDidMount() { expect(this.innerRef.current.getObject()).toEqual(innerObj); expect(this.outerRef.current.getObject()).toEqual(outerObj); mounted = true; } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(mounted).toBe(true); }); it('should support new-style refs with mixed-up owners', async () => { class Wrapper extends React.Component { getTitle = () => { return this.props.title; }; render() { return this.props.getContent(); } } let mounted = false; class Component extends React.Component { getInner = () => { // (With old-style refs, it's impossible to get a ref to this div // because Wrapper is the current owner when this function is called.) return
(this.innerRef = c)} />; }; render() { return ( (this.wrapperRef = c)} getContent={this.getInner} /> ); } componentDidMount() { // Check .props.title to make sure we got the right elements back expect(this.wrapperRef.getTitle()).toBe('wrapper'); expect(this.innerRef.className).toBe('inner'); mounted = true; } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(mounted).toBe(true); }); it('should call refs at the correct time', async () => { const log = []; class Inner extends React.Component { render() { log.push(`inner ${this.props.id} render`); return
; } componentDidMount() { log.push(`inner ${this.props.id} componentDidMount`); } componentDidUpdate() { log.push(`inner ${this.props.id} componentDidUpdate`); } componentWillUnmount() { log.push(`inner ${this.props.id} componentWillUnmount`); } } class Outer extends React.Component { render() { return (
{ log.push(`ref 1 got ${c ? `instance ${c.props.id}` : 'null'}`); }} /> { log.push(`ref 2 got ${c ? `instance ${c.props.id}` : 'null'}`); }} />
); } componentDidMount() { log.push('outer componentDidMount'); } componentDidUpdate() { log.push('outer componentDidUpdate'); } componentWillUnmount() { log.push('outer componentWillUnmount'); } } // mount, update, unmount const el = document.createElement('div'); log.push('start mount'); const root = ReactDOMClient.createRoot(el); await act(() => { root.render(); }); log.push('start update'); await act(() => { root.render(); }); log.push('start unmount'); await act(() => { root.unmount(); }); expect(log).toEqual([ 'start mount', 'inner 1 render', 'inner 2 render', 'inner 1 componentDidMount', 'ref 1 got instance 1', 'inner 2 componentDidMount', 'ref 2 got instance 2', 'outer componentDidMount', 'start update', // Previous (equivalent) refs get cleared // Fiber renders first, resets refs later 'inner 1 render', 'inner 2 render', 'ref 1 got null', 'ref 2 got null', 'inner 1 componentDidUpdate', 'ref 1 got instance 1', 'inner 2 componentDidUpdate', 'ref 2 got instance 2', 'outer componentDidUpdate', 'start unmount', 'outer componentWillUnmount', 'ref 1 got null', 'inner 1 componentWillUnmount', 'ref 2 got null', 'inner 2 componentWillUnmount', ]); }); // @gate !disableLegacyMode it('fires the callback after a component is rendered in legacy roots', () => { const callback = jest.fn(); const container = document.createElement('div'); ReactDOM.render(
, container, callback); expect(callback).toHaveBeenCalledTimes(1); ReactDOM.render(
, container, callback); expect(callback).toHaveBeenCalledTimes(2); ReactDOM.render(, container, callback); expect(callback).toHaveBeenCalledTimes(3); }); it('throws usefully when rendering badly-typed elements', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); const X = undefined; const XElement = ; if (gate(flags => !flags.enableOwnerStacks)) { assertConsoleErrorDev( [ 'React.jsx: type is invalid -- expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.', ], {withoutStack: true}, ); } await expect(async () => { await act(() => { root.render(XElement); }); }).rejects.toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.' + (__DEV__ ? " You likely forgot to export your component from the file it's " + 'defined in, or you might have mixed up default and named imports.' : ''), ); const Y = null; const YElement = ; if (gate(flags => !flags.enableOwnerStacks)) { assertConsoleErrorDev( [ 'React.jsx: type is invalid -- expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: null.', ], {withoutStack: true}, ); } await expect(async () => { await act(() => { root.render(YElement); }); }).rejects.toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: null.', ); const Z = true; const ZElement = ; if (gate(flags => !flags.enableOwnerStacks)) { assertConsoleErrorDev( [ 'React.jsx: type is invalid -- expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: boolean.', ], {withoutStack: true}, ); } await expect(async () => { await act(() => { root.render(ZElement); }); }).rejects.toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: boolean.', ); }); it('includes owner name in the error about badly-typed elements', async () => { const X = undefined; function Indirection(props) { return
{props.children}
; } function Bar() { return ( ); } function Foo() { return ; } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( expect(async () => { await act(() => { root.render(); }); }).toErrorDev( 'React.jsx: type is invalid -- expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.', ), ).rejects.toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.' + (__DEV__ ? " You likely forgot to export your component from the file it's " + 'defined in, or you might have mixed up default and named imports.' + '\n\nCheck the render method of `Bar`.' : ''), ); }); it('throws if a plain object is used as a child', async () => { const children = { x: , y: , z: , }; const element =
{[children]}
; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( act(() => { root.render(element); }), ).rejects.toThrowError( 'Objects are not valid as a React child (found: object with keys {x, y, z}). ' + 'If you meant to render a collection of children, use an array instead.', ); }); // @gate renameElementSymbol it('throws if a legacy element is used as a child', async () => { const inlinedElement = { $$typeof: Symbol.for('react.element'), type: 'div', key: null, ref: null, props: {}, _owner: null, }; const element =
{[inlinedElement]}
; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( act(() => { root.render(element); }), ).rejects.toThrowError( 'A React Element from an older version of React was rendered. ' + 'This is not supported. It can happen if:\n' + '- Multiple copies of the "react" package is used.\n' + '- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' + '- A compiler tries to "inline" JSX instead of using the runtime.', ); }); it('throws if a plain object even if it is in an owner', async () => { class Foo extends React.Component { render() { const children = { a: , b: , c: , }; return
{[children]}
; } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( act(() => { root.render(); }), ).rejects.toThrowError( 'Objects are not valid as a React child (found: object with keys {a, b, c}).' + ' If you meant to render a collection of children, use an array ' + 'instead.', ); }); it('throws if a plain object is used as a child when using SSR', async () => { const children = { x: , y: , z: , }; const element =
{[children]}
; expect(() => { ReactDOMServer.renderToString(element); }).toThrowError( 'Objects are not valid as a React child (found: object with keys {x, y, z}). ' + 'If you meant to render a collection of children, use ' + 'an array instead.', ); }); it('throws if a plain object even if it is in an owner when using SSR', async () => { class Foo extends React.Component { render() { const children = { a: , b: , c: , }; return
{[children]}
; } } const container = document.createElement('div'); expect(() => { ReactDOMServer.renderToString(, container); }).toThrowError( 'Objects are not valid as a React child (found: object with keys {a, b, c}). ' + 'If you meant to render a collection of children, use ' + 'an array instead.', ); }); describe('with new features', () => { it('warns on function as a return value from a function', async () => { function Foo() { return Foo; } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect(async () => { await act(() => { root.render(); }); }).toErrorDev( 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + ' {Foo}\n' + ' in Foo (at **)', ); }); it('warns on function as a return value from a class', async () => { class Foo extends React.Component { render() { return Foo; } } const container = document.createElement('div'); await expect(async () => { const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); }).toErrorDev( 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + ' {Foo}\n' + ' in Foo (at **)', ); }); it('warns on function as a child to host component', async () => { function Foo() { return (
{Foo}
); } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect(async () => { await act(() => { root.render(); }); }).toErrorDev( 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + ' {Foo}\n' + ' in span (at **)\n' + (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') + ' in Foo (at **)', ); }); it('does not warn for function-as-a-child that gets resolved', async () => { function Bar(props) { return props.children(); } function Foo() { return {() => 'Hello'}; } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); expect(container.innerHTML).toBe('Hello'); }); it('deduplicates function type warnings based on component type', async () => { class Foo extends React.PureComponent { constructor() { super(); this.state = {type: 'mushrooms'}; } render() { return (
{Foo} {Foo} {Foo} {Foo}
); } } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); let component; await expect(async () => { await act(() => { root.render( (component = current)} />); }); }).toErrorDev([ 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + '
{Foo}
\n' + ' in div (at **)\n' + ' in Foo (at **)', 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + ' {Foo}\n' + ' in span (at **)\n' + (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') + ' in Foo (at **)', ]); await act(() => { component.setState({type: 'portobello mushrooms'}); }); }); }); });