(instance1 = ref)} />);
+ });
expect(instance1.props.prop).toBe('testKey');
// We don't mutate the input, just in case the caller wants to do something
@@ -264,19 +322,24 @@ describe('ReactCompositeComponent', () => {
expect(inputProps.prop).not.toBeDefined();
});
- it('should warn about `forceUpdate` on not-yet-mounted components', () => {
+ it('should warn about `forceUpdate` on not-yet-mounted components', async () => {
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.forceUpdate();
}
render() {
- return ;
+ return foo
;
}
}
const container = document.createElement('div');
- expect(() => ReactDOM.render(, container)).toErrorDev(
+ const root = ReactDOMClient.createRoot(container);
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ }).toErrorDev(
"Warning: Can't call forceUpdate on a component that is not yet mounted. " +
'This is a no-op, but it might indicate a bug in your application. ' +
'Instead, assign to `this.state` directly or define a `state = {};` ' +
@@ -285,22 +348,32 @@ describe('ReactCompositeComponent', () => {
// No additional warning should be recorded
const container2 = document.createElement('div');
- ReactDOM.render(, container2);
+ const root2 = ReactDOMClient.createRoot(container2);
+ await act(() => {
+ root2.render();
+ });
+ expect(container2.firstChild.textContent).toBe('foo');
});
- it('should warn about `setState` on not-yet-mounted components', () => {
+ it('should warn about `setState` on not-yet-mounted components', async () => {
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.setState();
}
render() {
- return ;
+ return foo
;
}
}
const container = document.createElement('div');
- expect(() => ReactDOM.render(, container)).toErrorDev(
+ const root = ReactDOMClient.createRoot(container);
+
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ }).toErrorDev(
"Warning: Can't call setState on a component that is not yet mounted. " +
'This is a no-op, but it might indicate a bug in your application. ' +
'Instead, assign to `this.state` directly or define a `state = {};` ' +
@@ -309,67 +382,87 @@ describe('ReactCompositeComponent', () => {
// No additional warning should be recorded
const container2 = document.createElement('div');
- ReactDOM.render(, container2);
+ const root2 = ReactDOMClient.createRoot(container2);
+ await act(() => {
+ root2.render();
+ });
+ expect(container2.firstChild.textContent).toBe('foo');
});
- it('should not warn about `forceUpdate` on unmounted components', () => {
+ it('should not warn about `forceUpdate` on unmounted components', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
+ let instance;
class Component extends React.Component {
+ componentDidMount() {
+ instance = this;
+ }
+
render() {
return ;
}
}
- let instance = ;
- expect(instance.forceUpdate).not.toBeDefined();
+ const component = ;
+ expect(component.forceUpdate).not.toBeDefined();
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render(component);
+ });
- instance = ReactDOM.render(instance, container);
instance.forceUpdate();
- ReactDOM.unmountComponentAtNode(container);
+ root.unmount(container);
instance.forceUpdate();
instance.forceUpdate();
});
- it('should not warn about `setState` on unmounted components', () => {
+ it('should not warn about `setState` on unmounted components', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
- let renders = 0;
-
class Component extends React.Component {
state = {value: 0};
render() {
- renders++;
+ Scheduler.log('render ' + this.state.value);
return ;
}
}
- let instance;
- ReactDOM.render(
-
-
- (instance = c || instance)} />
-
-
,
- container,
- );
+ let ref;
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render(
+
+
+ (ref = c || ref)} />
+
+
,
+ );
+ });
- expect(renders).toBe(1);
+ assertLog(['render 0']);
- instance.setState({value: 1});
- expect(renders).toBe(2);
+ await act(() => {
+ ref.setState({value: 1});
+ });
+ assertLog(['render 1']);
- ReactDOM.render(, container);
- instance.setState({value: 2});
- expect(renders).toBe(2);
+ await act(() => {
+ root.render();
+ });
+
+ await act(() => {
+ ref.setState({value: 2});
+ });
+ // setState on an unmounted component is a noop.
+ assertLog([]);
});
- it('should silently allow `setState`, not call cb on unmounting components', () => {
+ it('should silently allow `setState`, not call cb on unmounting components', async () => {
let cbCalled = false;
const container = document.createElement('div');
document.body.appendChild(container);
@@ -389,24 +482,33 @@ describe('ReactCompositeComponent', () => {
return ;
}
}
-
- const instance = ReactDOM.render(, container);
+ let instance;
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render( (instance = c)} />);
+ });
+ await act(() => {
+ instance.setState({value: 1});
+ });
instance.setState({value: 1});
- ReactDOM.unmountComponentAtNode(container);
+ root.unmount();
expect(cbCalled).toBe(false);
});
- it('should warn when rendering a class with a render method that does not extend React.Component', () => {
+ it('should warn when rendering a class with a render method that does not extend React.Component', async () => {
const container = document.createElement('div');
class ClassWithRenderNotExtended {
render() {
return ;
}
}
+ const root = ReactDOMClient.createRoot(container);
expect(() => {
expect(() => {
- ReactDOM.render(, container);
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
}).toThrow(TypeError);
}).toErrorDev(
'Warning: The component appears to have a render method, ' +
@@ -416,33 +518,33 @@ describe('ReactCompositeComponent', () => {
// Test deduplication
expect(() => {
- ReactDOM.render(, container);
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
}).toThrow(TypeError);
});
- it('should warn about `setState` in render', () => {
+ it('should warn about `setState` in render', async () => {
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;
+ Scheduler.log('render ' + this.state.value);
if (this.state.value === 0) {
this.setState({value: 1});
}
- return ;
+ return foo {this.state.value}
;
}
}
let instance;
-
+ const root = ReactDOMClient.createRoot(container);
expect(() => {
- instance = ReactDOM.render(, container);
+ ReactDOM.flushSync(() => {
+ root.render( (instance = ref)} />);
+ });
}).toErrorDev(
'Cannot update during an existing state transition (such as within ' +
'`render`). Render methods should be a pure function of props and state.',
@@ -451,40 +553,37 @@ describe('ReactCompositeComponent', () => {
// The setState call is queued and then executed as a second pass. This
// behavior is undefined though so we're free to change it to suit the
// implementation details.
- expect(renderPasses).toBe(2);
- expect(renderedState).toBe(1);
+ assertLog(['render 0', 'render 1']);
expect(instance.state.value).toBe(1);
// Forcing a rerender anywhere will cause the update to happen.
- const instance2 = ReactDOM.render(, container);
- expect(instance).toBe(instance2);
- expect(renderedState).toBe(1);
- expect(instance2.state.value).toBe(1);
-
- // Test deduplication; (no additional warnings are expected).
- ReactDOM.unmountComponentAtNode(container);
- ReactDOM.render(, container);
+ await act(() => {
+ root.render();
+ });
+ assertLog(['render 1']);
});
- it('should cleanup even if render() fatals', () => {
+ it('should cleanup even if render() fatals', async () => {
class BadComponent extends React.Component {
render() {
throw new Error();
}
}
- let instance = ;
-
+ const instance = ;
expect(ReactCurrentOwner.current).toBe(null);
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
expect(() => {
- instance = ReactTestUtils.renderIntoDocument(instance);
+ ReactDOM.flushSync(() => {
+ root.render(instance);
+ });
}).toThrow();
expect(ReactCurrentOwner.current).toBe(null);
});
- it('should call componentWillUnmount before unmounting', () => {
+ it('should call componentWillUnmount before unmounting', async () => {
const container = document.createElement('div');
let innerUnmounted = false;
@@ -500,572 +599,149 @@ describe('ReactCompositeComponent', () => {
}
class Inner extends React.Component {
- componentWillUnmount() {
- innerUnmounted = true;
- }
-
- render() {
- return ;
- }
- }
-
- ReactDOM.render(, container);
- ReactDOM.unmountComponentAtNode(container);
- expect(innerUnmounted).toBe(true);
- });
-
- it('should warn when shouldComponentUpdate() returns undefined', () => {
- class ClassComponent extends React.Component {
- state = {bogus: false};
-
- shouldComponentUpdate() {
- return undefined;
- }
-
- render() {
- return ;
- }
- }
-
- const instance = ReactTestUtils.renderIntoDocument();
-
- expect(() => instance.setState({bogus: true})).toErrorDev(
- 'Warning: ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' +
- 'boolean value. Make sure to return true or false.',
- );
- });
-
- it('should warn when componentDidUnmount method is defined', () => {
- class Component extends React.Component {
- componentDidUnmount = () => {};
-
- render() {
- return ;
- }
- }
-
- expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev(
- 'Warning: Component has a method called ' +
- 'componentDidUnmount(). But there is no such lifecycle method. ' +
- 'Did you mean componentWillUnmount()?',
- );
- });
-
- it('should warn when componentDidReceiveProps method is defined', () => {
- class Component extends React.Component {
- componentDidReceiveProps = () => {};
-
- render() {
- return ;
- }
- }
-
- expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev(
- 'Warning: Component has a method called ' +
- 'componentDidReceiveProps(). But there is no such lifecycle method. ' +
- 'If you meant to update the state in response to changing props, ' +
- 'use componentWillReceiveProps(). If you meant to fetch data or ' +
- 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().',
- );
- });
-
- it('should warn when defaultProps was defined as an instance property', () => {
- class Component extends React.Component {
- constructor(props) {
- super(props);
- this.defaultProps = {name: 'Abhay'};
- }
-
- render() {
- return ;
- }
- }
-
- expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev(
- 'Warning: Setting defaultProps as an instance property on Component is not supported ' +
- 'and will be ignored. Instead, define defaultProps as a static property on Component.',
- );
- });
-
- // @gate !disableLegacyContext
- it('should pass context to children when not owner', () => {
- class Parent extends React.Component {
- render() {
- return (
-
-
-
- );
- }
- }
-
- 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 {this.context.foo}
;
- }
- }
-
- const component = ReactTestUtils.renderIntoDocument();
- expect(ReactDOM.findDOMNode(component).innerHTML).toBe('bar');
- });
-
- it('should skip update when rerendering element in container', () => {
- class Parent extends React.Component {
- render() {
- return {this.props.children}
;
- }
- }
-
- let childRenders = 0;
-
- class Child extends React.Component {
- render() {
- childRenders++;
- return ;
- }
- }
-
- const container = document.createElement('div');
- const child = ;
-
- ReactDOM.render({child}, container);
- ReactDOM.render({child}, container);
- expect(childRenders).toBe(1);
- });
-
- // @gate !disableLegacyContext
- it('should pass context when re-rendered for static child', () => {
- 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 Child;
- }
- }
-
- parentInstance = ReactTestUtils.renderIntoDocument(
-
-
-
-
- ,
- );
-
- expect(parentInstance.state.flag).toBe(false);
- expect(childInstance.context).toEqual({foo: 'bar', flag: false});
-
- parentInstance.setState({flag: true});
- expect(parentInstance.state.flag).toBe(true);
- expect(childInstance.context).toEqual({foo: 'bar', flag: true});
- });
-
- // @gate !disableLegacyContext
- it('should pass context when re-rendered for static child within a composite component', () => {
- class Parent extends React.Component {
- static childContextTypes = {
- flag: PropTypes.bool,
- };
-
- state = {
- flag: true,
- };
-
- getChildContext() {
- return {
- flag: this.state.flag,
- };
- }
-
- render() {
- return {this.props.children}
;
- }
- }
-
- class Child extends React.Component {
- static contextTypes = {
- flag: PropTypes.bool,
- };
-
- render() {
- return ;
- }
- }
-
- class Wrapper extends React.Component {
- parentRef = React.createRef();
- childRef = React.createRef();
-
- render() {
- return (
-
-
-
- );
- }
- }
-
- const wrapper = ReactTestUtils.renderIntoDocument();
-
- expect(wrapper.parentRef.current.state.flag).toEqual(true);
- expect(wrapper.childRef.current.context).toEqual({flag: true});
-
- // We update while is still a static prop relative to this update
- wrapper.parentRef.current.setState({flag: false});
-
- expect(wrapper.parentRef.current.state.flag).toEqual(false);
- expect(wrapper.childRef.current.context).toEqual({flag: false});
- });
-
- // @gate !disableLegacyContext
- it('should pass context transitively', () => {
- 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 ;
- }
- }
-
- 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 ;
- }
- }
-
- class Grandchild extends React.Component {
- static contextTypes = {
- foo: PropTypes.string,
- depth: PropTypes.number,
- };
-
- render() {
- grandchildInstance = this;
- return ;
- }
- }
-
- ReactTestUtils.renderIntoDocument();
- expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
- expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1});
- });
-
- // @gate !disableLegacyContext
- it('should pass context when re-rendered', () => {
- 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 = ;
- if (!this.state.flag) {
- output = Child;
- }
- return output;
- }
- }
-
- class Child extends React.Component {
- static contextTypes = {
- foo: PropTypes.string,
- depth: PropTypes.number,
- };
-
- render() {
- childInstance = this;
- return Child;
- }
- }
-
- parentInstance = ReactTestUtils.renderIntoDocument();
- expect(childInstance).toBeNull();
-
- expect(parentInstance.state.flag).toBe(false);
- ReactDOM.unstable_batchedUpdates(function () {
- parentInstance.setState({flag: true});
- });
- expect(parentInstance.state.flag).toBe(true);
-
- expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
- });
-
- // @gate !disableLegacyContext
- 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 {this.context.foo};
- }
- }
-
- 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 ;
- }
- }
-
- class Parent extends React.Component {
- static childContextTypes = {
- foo: PropTypes.string,
- };
-
- getChildContext() {
- return {
- foo: this.props.cntxt,
- };
+ componentWillUnmount() {
+ innerUnmounted = true;
}
render() {
- return ;
+ return ;
}
}
- const div = document.createElement('div');
- ReactDOM.render(, div);
- 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(, div);
- expect(div.children[0].innerHTML).toBe('bar');
- expect(div.children[0].id).toBe('aliens');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+ root.unmount();
+ expect(innerUnmounted).toBe(true);
});
- // @gate !disableLegacyContext
- 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++;
- }
+ it('should warn when shouldComponentUpdate() returns undefined', async () => {
+ class ClassComponent extends React.Component {
+ state = {bogus: false};
- if (nextContext !== this.context) {
- contextChanges++;
- }
+ shouldComponentUpdate() {
+ return undefined;
}
render() {
- return {this.props.children};
+ return ;
}
}
+ let instance;
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
+ await act(() => {
+ root.render( (instance = ref)} />);
+ });
- class ChildWithContext extends React.Component {
- static contextTypes = {
- foo: PropTypes.string.isRequired,
- };
-
- UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
- expect('foo' in nextContext).toBe(true);
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ instance.setState({bogus: true});
+ });
+ }).toErrorDev(
+ 'Warning: ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' +
+ 'boolean value. Make sure to return true or false.',
+ );
+ });
- if (nextProps !== this.props) {
- propChanges++;
- }
+ it('should warn when componentDidUnmount method is defined', async () => {
+ class Component extends React.Component {
+ componentDidUnmount = () => {};
- if (nextContext !== this.context) {
- contextChanges++;
- }
+ render() {
+ return ;
}
+ }
+
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ }).toErrorDev(
+ 'Warning: Component has a method called ' +
+ 'componentDidUnmount(). But there is no such lifecycle method. ' +
+ 'Did you mean componentWillUnmount()?',
+ );
+ });
+
+ it('should warn when componentDidReceiveProps method is defined', () => {
+ class Component extends React.Component {
+ componentDidReceiveProps = () => {};
render() {
- return {this.props.children}
;
+ return ;
}
}
- class ChildWithoutContext extends React.Component {
- UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
- expect('foo' in nextContext).toBe(false);
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
- if (nextProps !== this.props) {
- propChanges++;
- }
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ }).toErrorDev(
+ 'Warning: Component has a method called ' +
+ 'componentDidReceiveProps(). But there is no such lifecycle method. ' +
+ 'If you meant to update the state in response to changing props, ' +
+ 'use componentWillReceiveProps(). If you meant to fetch data or ' +
+ 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().',
+ );
+ });
- if (nextContext !== this.context) {
- contextChanges++;
- }
+ it('should warn when defaultProps was defined as an instance property', () => {
+ class Component extends React.Component {
+ constructor(props) {
+ super(props);
+ this.defaultProps = {name: 'Abhay'};
}
render() {
- return {this.props.children}
;
+ return ;
}
}
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
- class Parent extends React.Component {
- static childContextTypes = {
- foo: PropTypes.string,
- };
-
- state = {
- foo: 'abc',
- };
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ }).toErrorDev(
+ 'Warning: Setting defaultProps as an instance property on Component is not supported ' +
+ 'and will be ignored. Instead, define defaultProps as a static property on Component.',
+ );
+ });
- getChildContext() {
- return {
- foo: this.state.foo,
- };
+ it('should skip update when rerendering element in container', async () => {
+ class Parent extends React.Component {
+ render() {
+ return {this.props.children}
;
}
+ }
+ class Child extends React.Component {
render() {
- return {this.props.children}
;
+ Scheduler.log('Child render');
+ return ;
}
}
- const div = document.createElement('div');
-
- let parentInstance = null;
- ReactDOM.render(
- (parentInstance = inst)}>
-
- A1
- A2
-
-
-
- B1
- B2
-
- ,
- div,
- );
-
- parentInstance.setState({
- foo: 'def',
+ const container = document.createElement('div');
+ const child = ;
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render({child});
});
+ assertLog(['Child render']);
- expect(propChanges).toBe(0);
- expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2
+ await act(() => {
+ root.render({child});
+ });
+ assertLog([]);
});
it('should disallow nested render calls', () => {
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
class Inner extends React.Component {
render() {
return ;
@@ -1074,12 +750,16 @@ describe('ReactCompositeComponent', () => {
class Outer extends React.Component {
render() {
- ReactTestUtils.renderIntoDocument();
+ root.render();
return ;
}
}
- expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev(
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ }).toErrorDev(
'Render methods should be a pure function of props and state; ' +
'triggering nested component updates from render is not allowed. If ' +
'necessary, trigger nested updates in componentDidUpdate.\n\nCheck the ' +
@@ -1087,7 +767,7 @@ describe('ReactCompositeComponent', () => {
);
});
- it('only renders once if updated in componentWillReceiveProps', () => {
+ it('only renders once if updated in componentWillReceiveProps', async () => {
let renders = 0;
class Component extends React.Component {
@@ -1107,15 +787,23 @@ describe('ReactCompositeComponent', () => {
}
const container = document.createElement('div');
- const instance = ReactDOM.render(, container);
+ const root = ReactDOMClient.createRoot(container);
+ let instance;
+
+ await act(() => {
+ root.render( (instance = ref)} />);
+ });
expect(renders).toBe(1);
expect(instance.state.updated).toBe(false);
- ReactDOM.render(, container);
+
+ await act(() => {
+ root.render( (instance = ref)} />);
+ });
expect(renders).toBe(2);
expect(instance.state.updated).toBe(true);
});
- it('only renders once if updated in componentWillReceiveProps when batching', () => {
+ it('only renders once if updated in componentWillReceiveProps when batching', async () => {
let renders = 0;
class Component extends React.Component {
@@ -1135,234 +823,21 @@ describe('ReactCompositeComponent', () => {
}
const container = document.createElement('div');
- const instance = ReactDOM.render(, container);
+ const root = ReactDOMClient.createRoot(container);
+ let instance;
+ await act(() => {
+ root.render( (instance = ref)} />);
+ });
expect(renders).toBe(1);
expect(instance.state.updated).toBe(false);
- ReactDOM.unstable_batchedUpdates(() => {
- ReactDOM.render(, container);
+ await act(() => {
+ root.render( (instance = ref)} />);
});
expect(renders).toBe(2);
expect(instance.state.updated).toBe(true);
});
- it('should update refs if shouldComponentUpdate gives false', () => {
- class Static extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
-
- render() {
- return {this.props.children}
;
- }
- }
-
- class Component extends React.Component {
- static0Ref = React.createRef();
- static1Ref = React.createRef();
-
- render() {
- if (this.props.flipped) {
- return (
-
-
- B (ignored)
-
-
- A (ignored)
-
-
- );
- } else {
- return (
-
-
- A
-
-
- B
-
-
- );
- }
- }
- }
-
- const container = document.createElement('div');
- const comp = ReactDOM.render(, container);
- expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('A');
- expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('B');
-
- // When flipping the order, the refs should update even though the actual
- // contents do not
- ReactDOM.render(, container);
- expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('B');
- expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('A');
- });
-
- it('should allow access to findDOMNode in componentWillUnmount', () => {
- let a = null;
- let b = null;
-
- class Component extends React.Component {
- componentDidMount() {
- a = ReactDOM.findDOMNode(this);
- expect(a).not.toBe(null);
- }
-
- componentWillUnmount() {
- b = ReactDOM.findDOMNode(this);
- expect(b).not.toBe(null);
- }
-
- render() {
- return ;
- }
- }
-
- const container = document.createElement('div');
- expect(a).toBe(container.firstChild);
- ReactDOM.render(, container);
- ReactDOM.unmountComponentAtNode(container);
- expect(a).toBe(b);
- });
-
- // @gate !disableLegacyContext || !__DEV__
- 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 {this.props.children}
;
- }
- }
-
- class Component extends React.Component {
- static contextTypes = {
- foo: PropTypes.string.isRequired,
- };
-
- render() {
- return ;
- }
- }
-
- const div = document.createElement('div');
- ReactDOM.render(
-
-
- ,
- div,
- );
- });
-
- it('should replace state', () => {
- class Moo extends React.Component {
- state = {x: 1};
- render() {
- return ;
- }
- }
-
- const moo = ReactTestUtils.renderIntoDocument();
- // No longer a public API, but we can test that it works internally by
- // reaching into the updater.
- 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', () => {
- const NotActuallyImmutable = function (str) {
- this.str = str;
- };
- NotActuallyImmutable.prototype.amIImmutable = function () {
- return true;
- };
- class Moo extends React.Component {
- state = new NotActuallyImmutable('first');
- // No longer a public API, but we can test that it works internally by
- // reaching into the updater.
- _replaceState = update => this.updater.enqueueReplaceState(this, update);
- render() {
- return ;
- }
- }
-
- const moo = ReactTestUtils.renderIntoDocument();
- expect(moo.state.str).toBe('first');
- expect(moo.state.amIImmutable()).toBe(true);
-
- const secondState = new NotActuallyImmutable('second');
- moo._replaceState(secondState);
- expect(moo.state.str).toBe('second');
- expect(moo.state.amIImmutable()).toBe(true);
- expect(moo.state).toBe(secondState);
-
- moo.setState({str: 'third'});
- expect(moo.state.str).toBe('third');
- // Here we lose the prototype.
- expect(moo.state.amIImmutable).toBe(undefined);
-
- // When more than one state update is enqueued, we have the same behavior
- const fifthState = new NotActuallyImmutable('fifth');
- ReactDOM.unstable_batchedUpdates(function () {
- moo.setState({str: 'fourth'});
- moo._replaceState(fifthState);
- });
- expect(moo.state).toBe(fifthState);
-
- // When more than one state update is enqueued, we have the same behavior
- const sixthState = new NotActuallyImmutable('sixth');
- ReactDOM.unstable_batchedUpdates(function () {
- 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', () => {
- const container = document.createElement('div');
- const layer = document.createElement('div');
-
- class Component extends React.Component {
- componentDidMount() {
- ReactDOM.render(, layer);
- }
-
- componentWillUnmount() {
- ReactDOM.unmountComponentAtNode(layer);
- }
-
- render() {
- return ;
- }
- }
-
- class Outer extends React.Component {
- render() {
- return {this.props.children}
;
- }
- }
-
- ReactDOM.render(
-
-
- ,
- container,
- );
- ReactDOM.render(, container);
- });
-
- it('should warn when mutated props are passed', () => {
+ it('should warn when mutated props are passed', async () => {
const container = document.createElement('div');
class Foo extends React.Component {
@@ -1376,7 +851,12 @@ describe('ReactCompositeComponent', () => {
}
}
- expect(() => ReactDOM.render(, container)).toErrorDev(
+ const root = ReactDOMClient.createRoot(container);
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ }).toErrorDev(
'Foo(...): When calling super() in `Foo`, make sure to pass ' +
"up the same props that your component's constructor was passed.",
);
@@ -1416,29 +896,32 @@ describe('ReactCompositeComponent', () => {
}
};
+ const root = ReactDOMClient.createRoot(container);
expect(() => {
- ReactDOM.render(, container);
- ReactDOM.render(, container);
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
}).toThrow();
expect(count).toBe(1);
});
- it('prepares new child before unmounting old', () => {
- const log = [];
-
+ it('prepares new child before unmounting old', async () => {
class Spy extends React.Component {
UNSAFE_componentWillMount() {
- log.push(this.props.name + ' componentWillMount');
+ Scheduler.log(this.props.name + ' componentWillMount');
}
render() {
- log.push(this.props.name + ' render');
+ Scheduler.log(this.props.name + ' render');
return ;
}
componentDidMount() {
- log.push(this.props.name + ' componentDidMount');
+ Scheduler.log(this.props.name + ' componentDidMount');
}
componentWillUnmount() {
- log.push(this.props.name + ' componentWillUnmount');
+ Scheduler.log(this.props.name + ' componentWillUnmount');
}
}
@@ -1449,10 +932,15 @@ describe('ReactCompositeComponent', () => {
}
const container = document.createElement('div');
- ReactDOM.render(, container);
- ReactDOM.render(, container);
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+ await act(() => {
+ root.render();
+ });
- expect(log).toEqual([
+ assertLog([
'A componentWillMount',
'A render',
'A componentDidMount',
@@ -1464,8 +952,7 @@ describe('ReactCompositeComponent', () => {
]);
});
- it('respects a shallow shouldComponentUpdate implementation', () => {
- let renderCalls = 0;
+ it('respects a shallow shouldComponentUpdate implementation', async () => {
class PlasticWrap extends React.Component {
constructor(props, context) {
super(props, context);
@@ -1504,37 +991,54 @@ describe('ReactCompositeComponent', () => {
}
render() {
- renderCalls++;
+ const {color} = this.props;
+ const {cut, slices} = this.state;
+
+ Scheduler.log(`${color} ${cut} ${slices}`);
return ;
}
}
const container = document.createElement('div');
- const instance = ReactDOM.render(, container);
- expect(renderCalls).toBe(1);
+ const root = ReactDOMClient.createRoot(container);
+ let instance;
+ await act(() => {
+ root.render( (instance = ref)} />);
+ });
+ assertLog(['green false 1']);
// Do not re-render based on props
- instance.setState({color: 'green'});
- expect(renderCalls).toBe(1);
+ await act(() => {
+ instance.setState({color: 'green'});
+ });
+ assertLog([]);
// Re-render based on props
- instance.setState({color: 'red'});
- expect(renderCalls).toBe(2);
+ await act(() => {
+ instance.setState({color: 'red'});
+ });
+ assertLog(['red false 1']);
// Re-render base on state
- instance.appleRef.current.cut();
- expect(renderCalls).toBe(3);
+ await act(() => {
+ instance.appleRef.current.cut();
+ });
+ assertLog(['red true 10']);
// No re-render based on state
- instance.appleRef.current.cut();
- expect(renderCalls).toBe(3);
+ await act(() => {
+ instance.appleRef.current.cut();
+ });
+ assertLog([]);
// Re-render based on state again
- instance.appleRef.current.eatSlice();
- expect(renderCalls).toBe(4);
+ await act(() => {
+ instance.appleRef.current.eatSlice();
+ });
+ assertLog(['red true 9']);
});
- it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', () => {
+ it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', async () => {
function getInitialState() {
return {
foo: [1, 2, 3],
@@ -1542,7 +1046,6 @@ describe('ReactCompositeComponent', () => {
};
}
- let renderCalls = 0;
const initialSettings = getInitialState();
class Component extends React.Component {
@@ -1553,34 +1056,45 @@ describe('ReactCompositeComponent', () => {
}
render() {
- renderCalls++;
+ const {foo, bar} = this.state;
+ Scheduler.log(`{foo:[${foo}],bar:{a:${bar.a},b:${bar.b},c:${bar.c}}`);
return ;
}
}
const container = document.createElement('div');
- const instance = ReactDOM.render(, container);
- expect(renderCalls).toBe(1);
+ const root = ReactDOMClient.createRoot(container);
+ let instance;
+ await act(() => {
+ root.render( (instance = ref)} />);
+ });
+ assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
// Do not re-render if state is equal
const settings = {
foo: initialSettings.foo,
bar: initialSettings.bar,
};
- instance.setState(settings);
- expect(renderCalls).toBe(1);
+ await act(() => {
+ instance.setState(settings);
+ });
+ assertLog([]);
// Re-render because one field changed
initialSettings.foo = [1, 2, 3];
- instance.setState(initialSettings);
- expect(renderCalls).toBe(2);
+ await act(() => {
+ instance.setState(initialSettings);
+ });
+ assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
// Re-render because the object changed
- instance.setState(getInitialState());
- expect(renderCalls).toBe(3);
+ await act(() => {
+ instance.setState(getInitialState());
+ });
+ assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
});
- it('should call setState callback with no arguments', () => {
+ it('should call setState callback with no arguments', async () => {
let mockArgs;
class Component extends React.Component {
componentDidMount() {
@@ -1590,12 +1104,15 @@ describe('ReactCompositeComponent', () => {
return false;
}
}
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
+ await act(() => {
+ root.render();
+ });
- ReactTestUtils.renderIntoDocument();
expect(mockArgs.length).toEqual(0);
});
- it('this.state should be updated on setState callback inside componentWillMount', () => {
+ it('this.state should be updated on setState callback inside componentWillMount', async () => {
const div = document.createElement('div');
let stateSuccessfullyUpdated = false;
@@ -1619,16 +1136,18 @@ describe('ReactCompositeComponent', () => {
}
}
- ReactDOM.render(, div);
+ const root = ReactDOMClient.createRoot(div);
+ await act(() => {
+ root.render();
+ });
+
expect(stateSuccessfullyUpdated).toBe(true);
});
- it('should call the setState callback even if shouldComponentUpdate = false', done => {
+ it('should call the setState callback even if shouldComponentUpdate = false', async () => {
const mockFn = jest.fn().mockReturnValue(false);
const div = document.createElement('div');
- let instance;
-
class Component extends React.Component {
constructor(props, context) {
super(props, context);
@@ -1650,16 +1169,24 @@ describe('ReactCompositeComponent', () => {
}
}
- ReactDOM.render(, div);
+ const root = ReactDOMClient.createRoot(div);
+ let instance;
+ await act(() => {
+ root.render( (instance = ref)} />);
+ });
expect(instance).toBeDefined();
expect(mockFn).not.toBeCalled();
- instance.setState({hasUpdatedState: true}, () => {
- expect(mockFn).toBeCalled();
- expect(instance.state.hasUpdatedState).toBe(true);
- done();
+ await act(() => {
+ instance.setState({hasUpdatedState: true}, () => {
+ expect(mockFn).toBeCalled();
+ expect(instance.state.hasUpdatedState).toBe(true);
+ Scheduler.log('setState callback called');
+ });
});
+
+ assertLog(['setState callback called']);
});
it('should return a meaningful warning when constructor is returned', () => {
@@ -1674,9 +1201,12 @@ describe('ReactCompositeComponent', () => {
}
}
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
expect(() => {
expect(() => {
- ReactTestUtils.renderIntoDocument();
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
}).toThrow();
}).toErrorDev([
// Expect two errors because invokeGuardedCallback will dispatch an error event,
@@ -1685,6 +1215,11 @@ describe('ReactCompositeComponent', () => {
'did you accidentally return an object from the constructor?',
'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' +
'did you accidentally return an object from the constructor?',
+ // And then two more because we retry errors.
+ 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' +
+ 'did you accidentally return an object from the constructor?',
+ 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' +
+ 'did you accidentally return an object from the constructor?',
]);
});
@@ -1699,8 +1234,11 @@ describe('ReactCompositeComponent', () => {
}
const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
expect(() => {
- ReactDOM.render(, container);
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
}).toErrorDev(
'It looks like Bad is reassigning its own `this.props` while rendering. ' +
'This is not supported and can lead to confusing bugs.',
@@ -1710,9 +1248,12 @@ describe('ReactCompositeComponent', () => {
it('should return error if render is not defined', () => {
class RenderTestUndefinedRender extends React.Component {}
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
expect(() => {
expect(() => {
- ReactTestUtils.renderIntoDocument();
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
}).toThrow();
}).toErrorDev([
// Expect two errors because invokeGuardedCallback will dispatch an error event,
@@ -1721,12 +1262,18 @@ describe('ReactCompositeComponent', () => {
'component instance: you may have forgotten to define `render`.',
'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' +
'component instance: you may have forgotten to define `render`.',
+
+ // And then two more because we retry errors.
+ 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' +
+ 'component instance: you may have forgotten to define `render`.',
+ 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' +
+ 'component instance: you may have forgotten to define `render`.',
]);
});
// Regression test for accidental breaking change
// https://github.com/facebook/react/issues/13580
- it('should support classes shadowing isReactComponent', () => {
+ it('should support classes shadowing isReactComponent', async () => {
class Shadow extends React.Component {
isReactComponent() {}
render() {
@@ -1734,19 +1281,24 @@ describe('ReactCompositeComponent', () => {
}
}
const container = document.createElement('div');
- ReactDOM.render(, container);
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
expect(container.firstChild.tagName).toBe('DIV');
});
- it('should not warn on updating function component from componentWillMount', () => {
- let _setState;
+ it('should not warn on updating function component from componentWillMount', async () => {
+ let setState;
+ let ref;
function A() {
- _setState = React.useState()[1];
- return null;
+ const [state, _setState] = React.useState(null);
+ setState = _setState;
+ return (ref = r)}>{state}
;
}
class B extends React.Component {
UNSAFE_componentWillMount() {
- _setState({});
+ setState(1);
}
render() {
return null;
@@ -1761,18 +1313,25 @@ describe('ReactCompositeComponent', () => {
);
}
const container = document.createElement('div');
- ReactDOM.render(, container);
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(ref.textContent).toBe('1');
});
- it('should not warn on updating function component from componentWillUpdate', () => {
- let _setState;
+ it('should not warn on updating function component from componentWillUpdate', async () => {
+ let setState;
+ let ref;
function A() {
- _setState = React.useState()[1];
- return null;
+ const [state, _setState] = React.useState();
+ setState = _setState;
+ return (ref = r)}>{state}
;
}
class B extends React.Component {
UNSAFE_componentWillUpdate() {
- _setState({});
+ setState(1);
}
render() {
return null;
@@ -1787,19 +1346,29 @@ describe('ReactCompositeComponent', () => {
);
}
const container = document.createElement('div');
- ReactDOM.render(, container);
- ReactDOM.render(, container);
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+ await act(() => {
+ root.render();
+ });
+
+ expect(ref.textContent).toBe('1');
});
- it('should not warn on updating function component from componentWillReceiveProps', () => {
- let _setState;
+ it('should not warn on updating function component from componentWillReceiveProps', async () => {
+ let setState;
+ let ref;
function A() {
- _setState = React.useState()[1];
- return null;
+ const [state, _setState] = React.useState();
+ setState = _setState;
+ return (ref = r)}>{state}
;
}
+
class B extends React.Component {
UNSAFE_componentWillReceiveProps() {
- _setState({});
+ setState(1);
}
render() {
return null;
@@ -1814,19 +1383,29 @@ describe('ReactCompositeComponent', () => {
);
}
const container = document.createElement('div');
- ReactDOM.render(, container);
- ReactDOM.render(, container);
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+ await act(() => {
+ root.render();
+ });
+
+ expect(ref.textContent).toBe('1');
});
it('should warn on updating function component from render', () => {
- let _setState;
+ let setState;
+ let ref;
function A() {
- _setState = React.useState()[1];
- return null;
+ const [state, _setState] = React.useState(0);
+ setState = _setState;
+ return (ref = r)}>{state}
;
}
+
class B extends React.Component {
render() {
- _setState({});
+ setState(c => c + 1);
return null;
}
}
@@ -1839,12 +1418,24 @@ describe('ReactCompositeComponent', () => {
);
}
const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
expect(() => {
- ReactDOM.render(, container);
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
}).toErrorDev(
'Cannot update a component (`A`) while rendering a different component (`B`)',
);
+
+ // We error, but still update the state.
+ expect(ref.textContent).toBe('1');
+
// Dedupe.
- ReactDOM.render(, container);
+ ReactDOM.flushSync(() => {
+ root.render();
+ });
+
+ // We error, but still update the state.
+ expect(ref.textContent).toBe('2');
});
});
diff --git a/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js
new file mode 100644
index 0000000000000..d1f5493e95f44
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js
@@ -0,0 +1,800 @@
+/**
+ * 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 ReactTestUtils;
+let PropTypes;
+
+describe('ReactLegacyCompositeComponent', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactDOM = require('react-dom');
+ ReactTestUtils = require('react-dom/test-utils');
+ PropTypes = require('prop-types');
+ });
+
+ 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 ;
+ }
+ }
+
+ let instance;
+
+ expect(() => {
+ instance = ReactDOM.render(, container);
+ }).toErrorDev(
+ 'Cannot update during an existing state transition (such as within ' +
+ '`render`). Render methods should be a pure function of props and state.',
+ );
+
+ // The setState call is queued and then executed as a second pass. This
+ // behavior is undefined though so we're free to change it to suit the
+ // implementation details.
+ expect(renderPasses).toBe(2);
+ expect(renderedState).toBe(1);
+ expect(instance.state.value).toBe(1);
+
+ // Forcing a rerender anywhere will cause the update to happen.
+ const instance2 = ReactDOM.render(, container);
+ expect(instance).toBe(instance2);
+ expect(renderedState).toBe(1);
+ expect(instance2.state.value).toBe(1);
+
+ // Test deduplication; (no additional warnings are expected).
+ ReactDOM.unmountComponentAtNode(container);
+ ReactDOM.render(, container);
+ });
+
+ // @gate !disableLegacyContext
+ it('should pass context to children when not owner', () => {
+ class Parent extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ 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 {this.context.foo}
;
+ }
+ }
+
+ const component = ReactTestUtils.renderIntoDocument();
+ expect(ReactDOM.findDOMNode(component).innerHTML).toBe('bar');
+ });
+
+ // @gate !disableLegacyContext
+ it('should pass context when re-rendered for static child', () => {
+ 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 Child;
+ }
+ }
+
+ parentInstance = ReactTestUtils.renderIntoDocument(
+
+
+
+
+ ,
+ );
+
+ expect(parentInstance.state.flag).toBe(false);
+ expect(childInstance.context).toEqual({foo: 'bar', flag: false});
+
+ parentInstance.setState({flag: true});
+ expect(parentInstance.state.flag).toBe(true);
+ expect(childInstance.context).toEqual({foo: 'bar', flag: true});
+ });
+
+ // @gate !disableLegacyContext
+ it('should pass context when re-rendered for static child within a composite component', () => {
+ class Parent extends React.Component {
+ static childContextTypes = {
+ flag: PropTypes.bool,
+ };
+
+ state = {
+ flag: true,
+ };
+
+ getChildContext() {
+ return {
+ flag: this.state.flag,
+ };
+ }
+
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ class Child extends React.Component {
+ static contextTypes = {
+ flag: PropTypes.bool,
+ };
+
+ render() {
+ return ;
+ }
+ }
+
+ class Wrapper extends React.Component {
+ parentRef = React.createRef();
+ childRef = React.createRef();
+
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ const wrapper = ReactTestUtils.renderIntoDocument();
+
+ expect(wrapper.parentRef.current.state.flag).toEqual(true);
+ expect(wrapper.childRef.current.context).toEqual({flag: true});
+
+ // We update while is still a static prop relative to this update
+ wrapper.parentRef.current.setState({flag: false});
+
+ expect(wrapper.parentRef.current.state.flag).toEqual(false);
+ expect(wrapper.childRef.current.context).toEqual({flag: false});
+ });
+
+ // @gate !disableLegacyContext
+ it('should pass context transitively', () => {
+ 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 ;
+ }
+ }
+
+ 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 ;
+ }
+ }
+
+ class Grandchild extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string,
+ depth: PropTypes.number,
+ };
+
+ render() {
+ grandchildInstance = this;
+ return ;
+ }
+ }
+
+ ReactTestUtils.renderIntoDocument();
+ expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
+ expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1});
+ });
+
+ // @gate !disableLegacyContext
+ it('should pass context when re-rendered', () => {
+ 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 = ;
+ if (!this.state.flag) {
+ output = Child;
+ }
+ return output;
+ }
+ }
+
+ class Child extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string,
+ depth: PropTypes.number,
+ };
+
+ render() {
+ childInstance = this;
+ return Child;
+ }
+ }
+
+ parentInstance = ReactTestUtils.renderIntoDocument();
+ expect(childInstance).toBeNull();
+
+ expect(parentInstance.state.flag).toBe(false);
+ ReactDOM.unstable_batchedUpdates(function () {
+ parentInstance.setState({flag: true});
+ });
+ expect(parentInstance.state.flag).toBe(true);
+
+ expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
+ });
+
+ // @gate !disableLegacyContext
+ 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 {this.context.foo};
+ }
+ }
+
+ 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 ;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string,
+ };
+
+ getChildContext() {
+ return {
+ foo: this.props.cntxt,
+ };
+ }
+
+ render() {
+ return ;
+ }
+ }
+
+ const div = document.createElement('div');
+ ReactDOM.render(, div);
+ 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(, div);
+ expect(div.children[0].innerHTML).toBe('bar');
+ expect(div.children[0].id).toBe('aliens');
+ });
+
+ // @gate !disableLegacyContext
+ 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 {this.props.children};
+ }
+ }
+
+ 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 {this.props.children}
;
+ }
+ }
+
+ 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 {this.props.children}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string,
+ };
+
+ state = {
+ foo: 'abc',
+ };
+
+ getChildContext() {
+ return {
+ foo: this.state.foo,
+ };
+ }
+
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ const div = document.createElement('div');
+
+ let parentInstance = null;
+ ReactDOM.render(
+ (parentInstance = inst)}>
+
+ A1
+ A2
+
+
+
+ B1
+ B2
+
+ ,
+ div,
+ );
+
+ parentInstance.setState({
+ foo: 'def',
+ });
+
+ expect(propChanges).toBe(0);
+ expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2
+ });
+
+ 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 ;
+ }
+ }
+
+ const container = document.createElement('div');
+ const instance = ReactDOM.render(, container);
+ expect(renders).toBe(1);
+ expect(instance.state.updated).toBe(false);
+ ReactDOM.render(, 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 ;
+ }
+ }
+
+ const container = document.createElement('div');
+ const instance = ReactDOM.render(, container);
+ expect(renders).toBe(1);
+ expect(instance.state.updated).toBe(false);
+ ReactDOM.unstable_batchedUpdates(() => {
+ ReactDOM.render(, 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 {this.props.children}
;
+ }
+ }
+
+ class Component extends React.Component {
+ static0Ref = React.createRef();
+ static1Ref = React.createRef();
+
+ render() {
+ if (this.props.flipped) {
+ return (
+
+
+ B (ignored)
+
+
+ A (ignored)
+
+
+ );
+ } else {
+ return (
+
+
+ A
+
+
+ B
+
+
+ );
+ }
+ }
+ }
+
+ const container = document.createElement('div');
+ const comp = ReactDOM.render(, container);
+ expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('A');
+ expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('B');
+
+ // When flipping the order, the refs should update even though the actual
+ // contents do not
+ ReactDOM.render(, container);
+ expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('B');
+ expect(ReactDOM.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 = ReactDOM.findDOMNode(this);
+ expect(a).not.toBe(null);
+ }
+
+ componentWillUnmount() {
+ b = ReactDOM.findDOMNode(this);
+ expect(b).not.toBe(null);
+ }
+
+ render() {
+ return ;
+ }
+ }
+
+ const container = document.createElement('div');
+ expect(a).toBe(container.firstChild);
+ ReactDOM.render(, container);
+ ReactDOM.unmountComponentAtNode(container);
+ expect(a).toBe(b);
+ });
+
+ // @gate !disableLegacyContext || !__DEV__
+ 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 {this.props.children}
;
+ }
+ }
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string.isRequired,
+ };
+
+ render() {
+ return ;
+ }
+ }
+
+ const div = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ div,
+ );
+ });
+
+ it('should replace state in legacy mode', () => {
+ class Moo extends React.Component {
+ state = {x: 1};
+ render() {
+ return ;
+ }
+ }
+
+ const moo = ReactTestUtils.renderIntoDocument();
+ // No longer a public API, but we can test that it works internally by
+ // reaching into the updater.
+ 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', () => {
+ const NotActuallyImmutable = function (str) {
+ this.str = str;
+ };
+ NotActuallyImmutable.prototype.amIImmutable = function () {
+ return true;
+ };
+ class Moo extends React.Component {
+ state = new NotActuallyImmutable('first');
+ // No longer a public API, but we can test that it works internally by
+ // reaching into the updater.
+ _replaceState = update => this.updater.enqueueReplaceState(this, update);
+ render() {
+ return ;
+ }
+ }
+
+ const moo = ReactTestUtils.renderIntoDocument();
+ expect(moo.state.str).toBe('first');
+ expect(moo.state.amIImmutable()).toBe(true);
+
+ const secondState = new NotActuallyImmutable('second');
+ moo._replaceState(secondState);
+ expect(moo.state.str).toBe('second');
+ expect(moo.state.amIImmutable()).toBe(true);
+ expect(moo.state).toBe(secondState);
+
+ moo.setState({str: 'third'});
+ expect(moo.state.str).toBe('third');
+ // Here we lose the prototype.
+ expect(moo.state.amIImmutable).toBe(undefined);
+
+ // When more than one state update is enqueued, we have the same behavior
+ const fifthState = new NotActuallyImmutable('fifth');
+ ReactDOM.unstable_batchedUpdates(function () {
+ moo.setState({str: 'fourth'});
+ moo._replaceState(fifthState);
+ });
+ expect(moo.state).toBe(fifthState);
+
+ // When more than one state update is enqueued, we have the same behavior
+ const sixthState = new NotActuallyImmutable('sixth');
+ ReactDOM.unstable_batchedUpdates(function () {
+ 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(, layer);
+ }
+
+ componentWillUnmount() {
+ ReactDOM.unmountComponentAtNode(layer);
+ }
+
+ render() {
+ return ;
+ }
+ }
+
+ class Outer extends React.Component {
+ render() {
+ return {this.props.children}
;
+ }
+ }
+
+ ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ ReactDOM.render(, container);
+ });
+});