/**
* 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'});
});
});
});
});