'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 />;
}
}
let instance;
expect(() => {
instance = ReactDOM.render(<Component />, container);
}).toErrorDev(
'Cannot update during an existing state transition (such as within ' +
'`render`). Render methods should be a pure function of props and state.',
);
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.',
'Grandchild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
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.',
'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
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.',
'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
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.',
'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Grandchild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
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.',
]);
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.',
]);
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');
expect(() => {
ReactDOM.render(<Parent cntxt="noise" />, div);
}).toErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Leaf uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
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;
expect(() => {
ReactDOM.render(
<Parent ref={inst => (parentInstance = inst)}>
<ChildWithoutContext>
A1
<GrandChild>A2</GrandChild>
</ChildWithoutContext>
<ChildWithContext>
B1
<GrandChild>B2</GrandChild>
</ChildWithContext>
</Parent>,
div,
);
}).toErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'GrandChild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'ChildWithContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
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');
expect(() => {
ReactDOM.render(
<Parent>
<Component />
</Parent>,
div,
);
}).toErrorDev([
'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
});
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);
});
});