'use strict';
let React;
let ReactDOM;
let findDOMNode;
let ReactDOMClient;
let PropTypes;
let act;
let assertConsoleErrorDev;
describe('ReactLegacyCompositeComponent', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
findDOMNode =
ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
.findDOMNode;
PropTypes = require('prop-types');
({act, assertConsoleErrorDev} = require('internal-test-utils'));
});
it('should warn about `setState` in render in legacy mode', () => {
const container = document.createElement('div');
let renderedState = -1;
let renderPasses = 0;
class Component extends React.Component {
state = {value: 0};
render() {
renderPasses++;
renderedState = this.state.value;
if (this.state.value === 0) {
this.setState({value: 1});
}
return <div />;
}
}
const instance = ReactDOM.render(<Component />, container);
assertConsoleErrorDev([
'Cannot update during an existing state transition (such as within ' +
'`render`). Render methods should be a pure function of props and state.\n' +
' in Component (at **)',
]);
expect(renderPasses).toBe(2);
expect(renderedState).toBe(1);
expect(instance.state.value).toBe(1);
const instance2 = ReactDOM.render(<Component prop={123} />, container);
expect(instance).toBe(instance2);
expect(renderedState).toBe(1);
expect(instance2.state.value).toBe(1);
ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(<Component prop={123} />, container);
});
it('should pass context to children when not owner', async () => {
class Parent extends React.Component {
render() {
return (
<Child>
<Grandchild />
</Child>
);
}
}
class Child extends React.Component {
static childContextTypes = {
foo: PropTypes.string,
};
getChildContext() {
return {
foo: 'bar',
};
}
render() {
return React.Children.only(this.props.children);
}
}
class Grandchild extends React.Component {
static contextTypes = {
foo: PropTypes.string,
};
render() {
return <div>{this.context.foo}</div>;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let component;
await act(() => {
root.render(<Parent ref={current => (component = current)} />);
});
assertConsoleErrorDev([
'Child uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Grandchild uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
]);
expect(findDOMNode(component).innerHTML).toBe('bar');
});
it('should pass context when re-rendered for static child', async () => {
let parentInstance = null;
let childInstance = null;
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string,
flag: PropTypes.bool,
};
state = {
flag: false,
};
getChildContext() {
return {
foo: 'bar',
flag: this.state.flag,
};
}
render() {
return React.Children.only(this.props.children);
}
}
class Middle extends React.Component {
render() {
return this.props.children;
}
}
class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string,
flag: PropTypes.bool,
};
render() {
childInstance = this;
return <span>Child</span>;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<Parent ref={current => (parentInstance = current)}>
<Middle>
<Child />
</Middle>
</Parent>,
);
});
expect(parentInstance.state.flag).toBe(false);
expect(childInstance.context).toEqual({foo: 'bar', flag: false});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Child (at **)',
]);
await act(() => {
parentInstance.setState({flag: true});
});
expect(parentInstance.state.flag).toBe(true);
expect(childInstance.context).toEqual({foo: 'bar', flag: true});
});
it('should pass context when re-rendered for static child within a composite component', async () => {
class Parent extends React.Component {
static childContextTypes = {
flag: PropTypes.bool,
};
state = {
flag: true,
};
getChildContext() {
return {
flag: this.state.flag,
};
}
render() {
return <div>{this.props.children}</div>;
}
}
class Child extends React.Component {
static contextTypes = {
flag: PropTypes.bool,
};
render() {
return <div />;
}
}
class Wrapper extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
render() {
return (
<Parent ref={this.parentRef}>
<Child ref={this.childRef} />
</Parent>
);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let wrapper;
await act(() => {
root.render(<Wrapper ref={current => (wrapper = current)} />);
});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Wrapper (at **)',
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Wrapper (at **)',
]);
expect(wrapper.parentRef.current.state.flag).toEqual(true);
expect(wrapper.childRef.current.context).toEqual({flag: true});
await act(() => {
wrapper.parentRef.current.setState({flag: false});
});
expect(wrapper.parentRef.current.state.flag).toEqual(false);
expect(wrapper.childRef.current.context).toEqual({flag: false});
});
it('should pass context transitively', async () => {
let childInstance = null;
let grandchildInstance = null;
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string,
depth: PropTypes.number,
};
getChildContext() {
return {
foo: 'bar',
depth: 0,
};
}
render() {
return <Child />;
}
}
class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string,
depth: PropTypes.number,
};
static childContextTypes = {
depth: PropTypes.number,
};
getChildContext() {
return {
depth: this.context.depth + 1,
};
}
render() {
childInstance = this;
return <Grandchild />;
}
}
class Grandchild extends React.Component {
static contextTypes = {
foo: PropTypes.string,
depth: PropTypes.number,
};
render() {
grandchildInstance = this;
return <div />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Parent />);
});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Child uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Grandchild uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Child (at **)\n' +
' in Parent (at **)',
]);
expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1});
});
it('should pass context when re-rendered', async () => {
let parentInstance = null;
let childInstance = null;
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string,
depth: PropTypes.number,
};
state = {
flag: false,
};
getChildContext() {
return {
foo: 'bar',
depth: 0,
};
}
render() {
let output = <Child />;
if (!this.state.flag) {
output = <span>Child</span>;
}
return output;
}
}
class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string,
depth: PropTypes.number,
};
render() {
childInstance = this;
return <span>Child</span>;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Parent ref={current => (parentInstance = current)} />);
});
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
]);
expect(childInstance).toBeNull();
expect(parentInstance.state.flag).toBe(false);
await act(() => {
parentInstance.setState({flag: true});
});
assertConsoleErrorDev([
'Child uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
]);
expect(parentInstance.state.flag).toBe(true);
expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
});
it('unmasked context propagates through updates', () => {
class Leaf extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(true);
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
expect('foo' in nextContext).toBe(true);
return true;
}
render() {
return <span>{this.context.foo}</span>;
}
}
class Intermediary extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(false);
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
expect('foo' in nextContext).toBe(false);
return true;
}
render() {
return <Leaf />;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string,
};
getChildContext() {
return {
foo: this.props.cntxt,
};
}
render() {
return <Intermediary />;
}
}
const div = document.createElement('div');
ReactDOM.render(<Parent cntxt="noise" />, div);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Leaf uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Intermediary (at **)\n' +
' in Parent (at **)',
]);
expect(div.children[0].innerHTML).toBe('noise');
div.children[0].innerHTML = 'aliens';
div.children[0].id = 'aliens';
expect(div.children[0].innerHTML).toBe('aliens');
expect(div.children[0].id).toBe('aliens');
ReactDOM.render(<Parent cntxt="bar" />, div);
expect(div.children[0].innerHTML).toBe('bar');
expect(div.children[0].id).toBe('aliens');
});
it('should trigger componentWillReceiveProps for context changes', () => {
let contextChanges = 0;
let propChanges = 0;
class GrandChild extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(true);
if (nextProps !== this.props) {
propChanges++;
}
if (nextContext !== this.context) {
contextChanges++;
}
}
render() {
return <span className="grand-child">{this.props.children}</span>;
}
}
class ChildWithContext extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(true);
if (nextProps !== this.props) {
propChanges++;
}
if (nextContext !== this.context) {
contextChanges++;
}
}
render() {
return <div className="child-with">{this.props.children}</div>;
}
}
class ChildWithoutContext extends React.Component {
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(false);
if (nextProps !== this.props) {
propChanges++;
}
if (nextContext !== this.context) {
contextChanges++;
}
}
render() {
return <div className="child-without">{this.props.children}</div>;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string,
};
state = {
foo: 'abc',
};
getChildContext() {
return {
foo: this.state.foo,
};
}
render() {
return <div className="parent">{this.props.children}</div>;
}
}
const div = document.createElement('div');
let parentInstance = null;
ReactDOM.render(
<Parent ref={inst => (parentInstance = inst)}>
<ChildWithoutContext>
A1
<GrandChild>A2</GrandChild>
</ChildWithoutContext>
<ChildWithContext>
B1
<GrandChild>B2</GrandChild>
</ChildWithContext>
</Parent>,
div,
);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'GrandChild uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in GrandChild (at **)',
'ChildWithContext uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in ChildWithContext (at **)',
]);
parentInstance.setState({
foo: 'def',
});
expect(propChanges).toBe(0);
expect(contextChanges).toBe(3);
});
it('only renders once if updated in componentWillReceiveProps in legacy mode', () => {
let renders = 0;
class Component extends React.Component {
state = {updated: false};
UNSAFE_componentWillReceiveProps(props) {
expect(props.update).toBe(1);
expect(renders).toBe(1);
this.setState({updated: true});
expect(renders).toBe(1);
}
render() {
renders++;
return <div />;
}
}
const container = document.createElement('div');
const instance = ReactDOM.render(<Component update={0} />, container);
expect(renders).toBe(1);
expect(instance.state.updated).toBe(false);
ReactDOM.render(<Component update={1} />, container);
expect(renders).toBe(2);
expect(instance.state.updated).toBe(true);
});
it('only renders once if updated in componentWillReceiveProps when batching in legacy mode', () => {
let renders = 0;
class Component extends React.Component {
state = {updated: false};
UNSAFE_componentWillReceiveProps(props) {
expect(props.update).toBe(1);
expect(renders).toBe(1);
this.setState({updated: true});
expect(renders).toBe(1);
}
render() {
renders++;
return <div />;
}
}
const container = document.createElement('div');
const instance = ReactDOM.render(<Component update={0} />, container);
expect(renders).toBe(1);
expect(instance.state.updated).toBe(false);
ReactDOM.unstable_batchedUpdates(() => {
ReactDOM.render(<Component update={1} />, container);
});
expect(renders).toBe(2);
expect(instance.state.updated).toBe(true);
});
it('should update refs if shouldComponentUpdate gives false in legacy mode', () => {
class Static extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return <div>{this.props.children}</div>;
}
}
class Component extends React.Component {
static0Ref = React.createRef();
static1Ref = React.createRef();
render() {
if (this.props.flipped) {
return (
<div>
<Static ref={this.static0Ref} key="B">
B (ignored)
</Static>
<Static ref={this.static1Ref} key="A">
A (ignored)
</Static>
</div>
);
} else {
return (
<div>
<Static ref={this.static0Ref} key="A">
A
</Static>
<Static ref={this.static1Ref} key="B">
B
</Static>
</div>
);
}
}
}
const container = document.createElement('div');
const comp = ReactDOM.render(<Component flipped={false} />, container);
expect(findDOMNode(comp.static0Ref.current).textContent).toBe('A');
expect(findDOMNode(comp.static1Ref.current).textContent).toBe('B');
ReactDOM.render(<Component flipped={true} />, container);
expect(findDOMNode(comp.static0Ref.current).textContent).toBe('B');
expect(findDOMNode(comp.static1Ref.current).textContent).toBe('A');
});
it('should allow access to findDOMNode in componentWillUnmount in legacy mode', () => {
let a = null;
let b = null;
class Component extends React.Component {
componentDidMount() {
a = findDOMNode(this);
expect(a).not.toBe(null);
}
componentWillUnmount() {
b = findDOMNode(this);
expect(b).not.toBe(null);
}
render() {
return <div />;
}
}
const container = document.createElement('div');
expect(a).toBe(container.firstChild);
ReactDOM.render(<Component />, container);
ReactDOM.unmountComponentAtNode(container);
expect(a).toBe(b);
});
it('context should be passed down from the parent', () => {
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string,
};
getChildContext() {
return {
foo: 'bar',
};
}
render() {
return <div>{this.props.children}</div>;
}
}
class Component extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
render() {
return <div />;
}
}
const div = document.createElement('div');
ReactDOM.render(
<Parent>
<Component />
</Parent>,
div,
);
assertConsoleErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Parent (at **)',
'Component uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Component (at **)',
]);
});
it('should replace state in legacy mode', async () => {
class Moo extends React.Component {
state = {x: 1};
render() {
return <div />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let moo;
await act(() => {
root.render(<Moo ref={current => (moo = current)} />);
});
await act(() => {
moo.updater.enqueueReplaceState(moo, {y: 2});
});
expect('x' in moo.state).toBe(false);
expect(moo.state.y).toBe(2);
});
it('should support objects with prototypes as state in legacy mode', async () => {
const NotActuallyImmutable = function (str) {
this.str = str;
};
NotActuallyImmutable.prototype.amIImmutable = function () {
return true;
};
class Moo extends React.Component {
state = new NotActuallyImmutable('first');
_replaceState = update => this.updater.enqueueReplaceState(this, update);
render() {
return <div />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let moo;
await act(() => {
root.render(<Moo ref={current => (moo = current)} />);
});
expect(moo.state.str).toBe('first');
expect(moo.state.amIImmutable()).toBe(true);
const secondState = new NotActuallyImmutable('second');
await act(() => {
moo._replaceState(secondState);
});
expect(moo.state.str).toBe('second');
expect(moo.state.amIImmutable()).toBe(true);
expect(moo.state).toBe(secondState);
await act(() => {
moo.setState({str: 'third'});
});
expect(moo.state.str).toBe('third');
expect(moo.state.amIImmutable).toBe(undefined);
const fifthState = new NotActuallyImmutable('fifth');
await act(() => {
moo.setState({str: 'fourth'});
moo._replaceState(fifthState);
});
expect(moo.state).toBe(fifthState);
const sixthState = new NotActuallyImmutable('sixth');
await act(() => {
moo._replaceState(sixthState);
moo.setState({str: 'seventh'});
});
expect(moo.state.str).toBe('seventh');
expect(moo.state.amIImmutable).toBe(undefined);
});
it('should not warn about unmounting during unmounting in legacy mode', () => {
const container = document.createElement('div');
const layer = document.createElement('div');
class Component extends React.Component {
componentDidMount() {
ReactDOM.render(<div />, layer);
}
componentWillUnmount() {
ReactDOM.unmountComponentAtNode(layer);
}
render() {
return <div />;
}
}
class Outer extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
ReactDOM.render(
<Outer>
<Component />
</Outer>,
container,
);
ReactDOM.render(<Outer />, container);
});
});