'use strict';
let React;
let ReactNoop;
let Scheduler;
let PropTypes;
let assertConsoleErrorDev;
let waitForAll;
let waitFor;
let waitForThrow;
let assertLog;
describe('ReactIncremental', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
PropTypes = require('prop-types');
({
assertConsoleErrorDev,
waitForAll,
waitFor,
waitForThrow,
assertLog,
} = require('internal-test-utils'));
});
function LegacyHiddenDiv({children, mode}) {
return (
<div hidden={mode === 'hidden'}>
<React.unstable_LegacyHidden
mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
{children}
</React.unstable_LegacyHidden>
</div>
);
}
it('should render a simple component', async () => {
function Bar() {
return <div>Hello World</div>;
}
function Foo() {
return <Bar isBar={true} />;
}
ReactNoop.render(<Foo />);
await waitForAll([]);
});
it('should render a simple component, in steps if needed', async () => {
function Bar() {
Scheduler.log('Bar');
return (
<span>
<div>Hello World</div>
</span>
);
}
function Foo() {
Scheduler.log('Foo');
return [<Bar key="a" isBar={true} />, <Bar key="b" isBar={true} />];
}
React.startTransition(() => {
ReactNoop.render(<Foo />, () => Scheduler.log('callback'));
});
await waitFor(['Foo']);
await waitForAll(['Bar', 'Bar', 'callback']);
});
it('updates a previous render', async () => {
function Header() {
Scheduler.log('Header');
return <h1>Hi</h1>;
}
function Content(props) {
Scheduler.log('Content');
return <div>{props.children}</div>;
}
function Footer() {
Scheduler.log('Footer');
return <footer>Bye</footer>;
}
const header = <Header />;
const footer = <Footer />;
function Foo(props) {
Scheduler.log('Foo');
return (
<div>
{header}
<Content>{props.text}</Content>
{footer}
</div>
);
}
ReactNoop.render(<Foo text="foo" />, () =>
Scheduler.log('renderCallbackCalled'),
);
await waitForAll([
'Foo',
'Header',
'Content',
'Footer',
'renderCallbackCalled',
]);
ReactNoop.render(<Foo text="bar" />, () =>
Scheduler.log('firstRenderCallbackCalled'),
);
ReactNoop.render(<Foo text="bar" />, () =>
Scheduler.log('secondRenderCallbackCalled'),
);
await waitForAll([
'Foo',
'Content',
'firstRenderCallbackCalled',
'secondRenderCallbackCalled',
]);
});
it('can cancel partially rendered work and restart', async () => {
function Bar(props) {
Scheduler.log('Bar');
return <div>{props.children}</div>;
}
function Foo(props) {
Scheduler.log('Foo');
return (
<div>
<Bar>{props.text}</Bar>
<Bar>{props.text}</Bar>
</div>
);
}
ReactNoop.render(<Foo text="foo" />);
await waitForAll(['Foo', 'Bar', 'Bar']);
React.startTransition(() => {
ReactNoop.render(<Foo text="bar" />);
});
await waitFor(['Foo', 'Bar']);
ReactNoop.flushSync(() => ReactNoop.render(null));
React.startTransition(() => {
ReactNoop.render(<Foo text="baz" />);
});
await waitFor(['Foo', 'Bar']);
await waitForAll(['Bar']);
});
it('should call callbacks even if updates are aborted', async () => {
let inst;
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
text: 'foo',
text2: 'foo',
};
inst = this;
}
render() {
return (
<div>
<div>{this.state.text}</div>
<div>{this.state.text2}</div>
</div>
);
}
}
ReactNoop.render(<Foo />);
await waitForAll([]);
React.startTransition(() => {
inst.setState(
() => {
Scheduler.log('setState1');
return {text: 'bar'};
},
() => Scheduler.log('callback1'),
);
});
await waitFor(['setState1']);
ReactNoop.flushSync(() => ReactNoop.render(<Foo />));
React.startTransition(() => {
inst.setState(
() => {
Scheduler.log('setState2');
return {text2: 'baz'};
},
() => Scheduler.log('callback2'),
);
});
await waitForAll(['setState1', 'setState2', 'callback1', 'callback2']);
expect(inst.state).toEqual({text: 'bar', text2: 'baz'});
});
it('can deprioritize unfinished work and resume it later', async () => {
function Bar(props) {
Scheduler.log('Bar');
return <div>{props.children}</div>;
}
function Middle(props) {
Scheduler.log('Middle');
return <span>{props.children}</span>;
}
function Foo(props) {
Scheduler.log('Foo');
return (
<div>
<Bar>{props.text}</Bar>
<LegacyHiddenDiv mode="hidden">
<Middle>{props.text}</Middle>
</LegacyHiddenDiv>
<Bar>{props.text}</Bar>
<LegacyHiddenDiv mode="hidden">
<Middle>Footer</Middle>
</LegacyHiddenDiv>
</div>
);
}
ReactNoop.render(<Foo text="foo" />);
await waitForAll(['Foo', 'Bar', 'Bar', 'Middle', 'Middle']);
ReactNoop.render(<Foo text="bar" />);
await waitFor(['Foo', 'Bar', 'Bar']);
await waitForAll(['Middle', 'Middle']);
});
it('can deprioritize a tree from without dropping work', async () => {
function Bar(props) {
Scheduler.log('Bar');
return <div>{props.children}</div>;
}
function Middle(props) {
Scheduler.log('Middle');
return <span>{props.children}</span>;
}
function Foo(props) {
Scheduler.log('Foo');
return (
<div>
<Bar>{props.text}</Bar>
<LegacyHiddenDiv mode="hidden">
<Middle>{props.text}</Middle>
</LegacyHiddenDiv>
<Bar>{props.text}</Bar>
<LegacyHiddenDiv mode="hidden">
<Middle>Footer</Middle>
</LegacyHiddenDiv>
</div>
);
}
ReactNoop.flushSync(() => {
ReactNoop.render(<Foo text="foo" />);
});
assertLog(['Foo', 'Bar', 'Bar']);
await waitForAll(['Middle', 'Middle']);
ReactNoop.flushSync(() => {
ReactNoop.render(<Foo text="foo" />);
});
assertLog(['Foo', 'Bar', 'Bar']);
await waitForAll(['Middle', 'Middle']);
});
it.skip('can resume work in a subtree even when a parent bails out', async () => {
function Bar(props) {
Scheduler.log('Bar');
return <div>{props.children}</div>;
}
function Tester() {
Scheduler.log('Tester');
return <div />;
}
function Middle(props) {
Scheduler.log('Middle');
return <span>{props.children}</span>;
}
const middleContent = (
<aaa>
<Tester />
<bbb hidden={true}>
<ccc>
<Middle>Hi</Middle>
</ccc>
</bbb>
</aaa>
);
function Foo(props) {
Scheduler.log('Foo');
return (
<div>
<Bar>{props.text}</Bar>
{middleContent}
<Bar>{props.text}</Bar>
</div>
);
}
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flushDeferredPri(52);
assertLog(['Foo', 'Bar', 'Tester', 'Bar']);
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flushDeferredPri(45 + 5);
assertLog(['Foo', 'Bar', 'Bar']);
await waitForAll(['Middle']);
});
it.skip('can resume work in a bailed subtree within one pass', async () => {
function Bar(props) {
Scheduler.log('Bar');
return <div>{props.children}</div>;
}
class Tester extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
Scheduler.log('Tester');
return <div />;
}
}
function Middle(props) {
Scheduler.log('Middle');
return <span>{props.children}</span>;
}
class Content extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return [
<Tester key="a" unused={this.props.unused} />,
<bbb key="b" hidden={true}>
<ccc>
<Middle>Hi</Middle>
</ccc>
</bbb>,
];
}
}
function Foo(props) {
Scheduler.log('Foo');
return (
<div hidden={props.text === 'bar'}>
<Bar>{props.text}</Bar>
<Content unused={props.text} />
<Bar>{props.text}</Bar>
</div>
);
}
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flushDeferredPri(52 + 5);
assertLog(['Foo', 'Bar', 'Tester', 'Bar']);
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flushDeferredPri(15);
assertLog(['Foo']);
await waitForAll(['Bar', 'Middle', 'Bar']);
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flushDeferredPri(40);
assertLog(['Foo', 'Bar']);
ReactNoop.render(<Foo text="bar" />);
await waitForAll(['Foo', 'Bar', 'Bar']);
});
it.skip('can resume mounting a class component', async () => {
let foo;
class Parent extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return <Foo prop={this.props.prop} />;
}
}
class Foo extends React.Component {
constructor(props) {
super(props);
Scheduler.log('Foo constructor: ' + props.prop);
}
render() {
foo = this;
Scheduler.log('Foo');
return <Bar />;
}
}
function Bar() {
Scheduler.log('Bar');
return <div />;
}
ReactNoop.render(<Parent prop="foo" />);
ReactNoop.flushDeferredPri(20);
assertLog(['Foo constructor: foo', 'Foo']);
foo.setState({value: 'bar'});
await waitForAll(['Foo', 'Bar']);
});
it.skip('reuses the same instance when resuming a class instance', async () => {
let foo;
class Parent extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return <Foo prop={this.props.prop} />;
}
}
let constructorCount = 0;
class Foo extends React.Component {
constructor(props) {
super(props);
Scheduler.log('constructor: ' + props.prop);
constructorCount++;
}
UNSAFE_componentWillMount() {
Scheduler.log('componentWillMount: ' + this.props.prop);
}
UNSAFE_componentWillReceiveProps() {
Scheduler.log('componentWillReceiveProps: ' + this.props.prop);
}
componentDidMount() {
Scheduler.log('componentDidMount: ' + this.props.prop);
}
UNSAFE_componentWillUpdate() {
Scheduler.log('componentWillUpdate: ' + this.props.prop);
}
componentDidUpdate() {
Scheduler.log('componentDidUpdate: ' + this.props.prop);
}
render() {
foo = this;
Scheduler.log('render: ' + this.props.prop);
return <Bar />;
}
}
function Bar() {
Scheduler.log('Foo did complete');
return <div />;
}
ReactNoop.render(<Parent prop="foo" />);
ReactNoop.flushDeferredPri(25);
assertLog([
'constructor: foo',
'componentWillMount: foo',
'render: foo',
'Foo did complete',
]);
foo.setState({value: 'bar'});
await waitForAll([]);
expect(constructorCount).toEqual(1);
assertLog([
'componentWillMount: foo',
'render: foo',
'Foo did complete',
'componentDidMount: foo',
]);
});
it.skip('can reuse work done after being preempted', async () => {
function Bar(props) {
Scheduler.log('Bar');
return <div>{props.children}</div>;
}
function Middle(props) {
Scheduler.log('Middle');
return <span>{props.children}</span>;
}
const middleContent = (
<div>
<Middle>Hello</Middle>
<Bar>-</Bar>
<Middle>World</Middle>
</div>
);
const step0 = (
<div>
<Middle>Hi</Middle>
<Bar>{'Foo'}</Bar>
<Middle>There</Middle>
</div>
);
function Foo(props) {
Scheduler.log('Foo');
return (
<div>
<Bar>{props.text2}</Bar>
<div hidden={true}>{props.step === 0 ? step0 : middleContent}</div>
</div>
);
}
ReactNoop.render(<Foo text="foo" text2="foo" step={0} />);
ReactNoop.flushDeferredPri(55 + 25 + 5 + 5);
assertLog(['Foo', 'Bar', 'Middle', 'Bar']);
ReactNoop.render(<Foo text="foo" text2="bar" step={0} />);
await waitForAll([]);
assertLog(['Foo', 'Bar', 'Middle']);
ReactNoop.render(<Foo text="bar" text2="bar" step={1} />);
ReactNoop.flushDeferredPri(30 + 25 + 5);
assertLog(['Foo', 'Bar']);
ReactNoop.flushDeferredPri(30 + 5);
assertLog(['Middle', 'Bar']);
ReactNoop.render(<Foo text="foo" text2="bar" step={1} />);
ReactNoop.flushDeferredPri(30);
assertLog(['Foo', 'Bar']);
await waitForAll(['Middle']);
});
it.skip('can reuse work that began but did not complete, after being preempted', async () => {
let child;
let sibling;
function GreatGrandchild() {
Scheduler.log('GreatGrandchild');
return <div />;
}
function Grandchild() {
Scheduler.log('Grandchild');
return <GreatGrandchild />;
}
class Child extends React.Component {
state = {step: 0};
render() {
child = this;
Scheduler.log('Child');
return <Grandchild />;
}
}
class Sibling extends React.Component {
render() {
Scheduler.log('Sibling');
sibling = this;
return <div />;
}
}
function Parent() {
Scheduler.log('Parent');
return [
<div key="a">
<Child />
</div>,
<Sibling key="b" />,
];
}
ReactNoop.render(<Parent />);
await waitForAll([]);
child.setState({step: 1});
ReactNoop.flushDeferredPri(30);
assertLog(['Child', 'Grandchild']);
ReactNoop.flushSync(() => {
sibling.setState({});
});
assertLog(['Sibling']);
await waitForAll([
'GreatGrandchild',
]);
});
it.skip('can reuse work if shouldComponentUpdate is false, after being preempted', async () => {
function Bar(props) {
Scheduler.log('Bar');
return <div>{props.children}</div>;
}
class Middle extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.children !== nextProps.children;
}
render() {
Scheduler.log('Middle');
return <span>{this.props.children}</span>;
}
}
class Content extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.step !== nextProps.step;
}
render() {
Scheduler.log('Content');
return (
<div>
<Middle>{this.props.step === 0 ? 'Hi' : 'Hello'}</Middle>
<Bar>{this.props.step === 0 ? this.props.text : '-'}</Bar>
<Middle>{this.props.step === 0 ? 'There' : 'World'}</Middle>
</div>
);
}
}
function Foo(props) {
Scheduler.log('Foo');
return (
<div>
<Bar>{props.text}</Bar>
<div hidden={true}>
<Content step={props.step} text={props.text} />
</div>
</div>
);
}
ReactNoop.render(<Foo text="foo" step={0} />);
await waitForAll(['Foo', 'Bar', 'Content', 'Middle', 'Bar', 'Middle']);
ReactNoop.render(<Foo text="bar" step={1} />);
ReactNoop.flushDeferredPri(30 + 5);
assertLog(['Foo', 'Bar']);
ReactNoop.flushDeferredPri(30 + 25 + 5);
assertLog(['Content', 'Middle', 'Bar']);
ReactNoop.render(<Foo text="foo" step={1} />);
ReactNoop.flushDeferredPri(30);
assertLog(['Foo', 'Bar']);
await waitForAll(['Middle']);
});
it('memoizes work even if shouldComponentUpdate returns false', async () => {
class Foo extends React.Component {
shouldComponentUpdate(nextProps) {
const shouldUpdate = this.props.step !== 1;
Scheduler.log('shouldComponentUpdate: ' + shouldUpdate);
return shouldUpdate;
}
render() {
Scheduler.log('render');
return <div />;
}
}
ReactNoop.render(<Foo step={1} />);
await waitForAll(['render']);
ReactNoop.render(<Foo step={2} />);
await waitForAll(['shouldComponentUpdate: false']);
ReactNoop.render(<Foo step={3} />);
await waitForAll([
'shouldComponentUpdate: true',
'render',
]);
});
it('can update in the middle of a tree using setState', async () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = {a: 'a'};
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo() {
return (
<div>
<Bar />
</div>
);
}
ReactNoop.render(<Foo />);
await waitForAll([]);
expect(instance.state).toEqual({a: 'a'});
instance.setState({b: 'b'});
await waitForAll([]);
expect(instance.state).toEqual({a: 'a', b: 'b'});
});
it('can queue multiple state updates', async () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = {a: 'a'};
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo() {
return (
<div>
<Bar />
</div>
);
}
ReactNoop.render(<Foo />);
await waitForAll([]);
instance.setState({b: 'b'});
instance.setState({c: 'c'});
instance.setState({d: 'd'});
await waitForAll([]);
expect(instance.state).toEqual({a: 'a', b: 'b', c: 'c', d: 'd'});
});
it('can use updater form of setState', async () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = {num: 1};
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo({multiplier}) {
return (
<div>
<Bar multiplier={multiplier} />
</div>
);
}
function updater(state, props) {
return {num: state.num * props.multiplier};
}
ReactNoop.render(<Foo multiplier={2} />);
await waitForAll([]);
expect(instance.state.num).toEqual(1);
instance.setState(updater);
await waitForAll([]);
expect(instance.state.num).toEqual(2);
instance.setState(updater);
ReactNoop.render(<Foo multiplier={3} />);
await waitForAll([]);
expect(instance.state.num).toEqual(6);
});
it('can call setState inside update callback', async () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = {num: 1};
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo({multiplier}) {
return (
<div>
<Bar multiplier={multiplier} />
</div>
);
}
function updater(state, props) {
return {num: state.num * props.multiplier};
}
function callback() {
this.setState({called: true});
}
ReactNoop.render(<Foo multiplier={2} />);
await waitForAll([]);
instance.setState(updater);
instance.setState(updater, callback);
await waitForAll([]);
expect(instance.state.num).toEqual(4);
expect(instance.state.called).toEqual(true);
});
it('can replaceState', async () => {
let instance;
class Bar extends React.Component {
state = {a: 'a'};
render() {
instance = this;
return <div>{this.props.children}</div>;
}
}
function Foo() {
return (
<div>
<Bar />
</div>
);
}
ReactNoop.render(<Foo />);
await waitForAll([]);
instance.setState({b: 'b'});
instance.setState({c: 'c'});
instance.updater.enqueueReplaceState(instance, {d: 'd'});
await waitForAll([]);
expect(instance.state).toEqual({d: 'd'});
});
it('can forceUpdate', async () => {
function Baz() {
Scheduler.log('Baz');
return <div />;
}
let instance;
class Bar extends React.Component {
constructor() {
super();
instance = this;
}
shouldComponentUpdate() {
return false;
}
render() {
Scheduler.log('Bar');
return <Baz />;
}
}
function Foo() {
Scheduler.log('Foo');
return (
<div>
<Bar />
</div>
);
}
ReactNoop.render(<Foo />);
await waitForAll(['Foo', 'Bar', 'Baz']);
instance.forceUpdate();
await waitForAll(['Bar', 'Baz']);
});
it('should clear forceUpdate after update is flushed', async () => {
let a = 0;
class Foo extends React.PureComponent {
render() {
const msg = `A: ${a}, B: ${this.props.b}`;
Scheduler.log(msg);
return msg;
}
}
const foo = React.createRef(null);
ReactNoop.render(<Foo ref={foo} b={0} />);
await waitForAll(['A: 0, B: 0']);
a = 1;
foo.current.forceUpdate();
await waitForAll(['A: 1, B: 0']);
ReactNoop.render(<Foo ref={foo} b={0} />);
await waitForAll([]);
});
it.skip('can call sCU while resuming a partly mounted component', () => {
const instances = new Set();
class Bar extends React.Component {
state = {y: 'A'};
constructor() {
super();
instances.add(this);
}
shouldComponentUpdate(newProps, newState) {
return this.props.x !== newProps.x || this.state.y !== newState.y;
}
render() {
Scheduler.log('Bar:' + this.props.x);
return <span prop={String(this.props.x === this.state.y)} />;
}
}
function Foo(props) {
Scheduler.log('Foo');
return [
<Bar key="a" x="A" />,
<Bar key="b" x={props.step === 0 ? 'B' : 'B2'} />,
<Bar key="c" x="C" />,
<Bar key="d" x="D" />,
];
}
ReactNoop.render(<Foo step={0} />);
ReactNoop.flushDeferredPri(40);
assertLog(['Foo', 'Bar:A', 'Bar:B', 'Bar:C']);
expect(instances.size).toBe(3);
ReactNoop.render(<Foo step={1} />);
ReactNoop.flushDeferredPri(50);
assertLog(['Foo', 'Bar:B2', 'Bar:D']);
expect(instances.size).toBe(4);
});
it.skip('gets new props when setting state on a partly updated component', async () => {
const instances = [];
class Bar extends React.Component {
state = {y: 'A'};
constructor() {
super();
instances.push(this);
}
performAction() {
this.setState({
y: 'B',
});
}
render() {
Scheduler.log('Bar:' + this.props.x + '-' + this.props.step);
return <span prop={String(this.props.x === this.state.y)} />;
}
}
function Baz() {
Scheduler.log('Baz');
return <div />;
}
function Foo(props) {
Scheduler.log('Foo');
return [
<Bar key="a" x="A" step={props.step} />,
<Bar key="b" x="B" step={props.step} />,
];
}
ReactNoop.render(
<div>
<Foo step={0} />
<Baz />
<Baz />
</div>,
);
await waitForAll([]);
ReactNoop.render(
<div>
<Foo step={1} />
<Baz />
<Baz />
</div>,
);
ReactNoop.flushDeferredPri(45);
assertLog(['Foo', 'Bar:A-1', 'Bar:B-1', 'Baz']);
instances[0].performAction();
await waitForAll(['Bar:A-1', 'Baz']);
});
it.skip('calls componentWillMount twice if the initial render is aborted', async () => {
class LifeCycle extends React.Component {
state = {x: this.props.x};
UNSAFE_componentWillReceiveProps(nextProps) {
Scheduler.log(
'componentWillReceiveProps:' + this.state.x + '-' + nextProps.x,
);
this.setState({x: nextProps.x});
}
UNSAFE_componentWillMount() {
Scheduler.log(
'componentWillMount:' + this.state.x + '-' + this.props.x,
);
}
componentDidMount() {
Scheduler.log('componentDidMount:' + this.state.x + '-' + this.props.x);
}
render() {
return <span />;
}
}
function Trail() {
Scheduler.log('Trail');
return null;
}
function App(props) {
Scheduler.log('App');
return (
<div>
<LifeCycle x={props.x} />
<Trail />
</div>
);
}
ReactNoop.render(<App x={0} />);
ReactNoop.flushDeferredPri(30);
assertLog(['App', 'componentWillMount:0-0']);
ReactNoop.render(<App x={1} />);
await waitForAll([
'App',
'componentWillReceiveProps:0-1',
'componentWillMount:1-1',
'Trail',
'componentDidMount:1-1',
]);
});
it.skip('uses state set in componentWillMount even if initial render was aborted', async () => {
class LifeCycle extends React.Component {
constructor(props) {
super(props);
this.state = {x: this.props.x + '(ctor)'};
}
UNSAFE_componentWillMount() {
Scheduler.log('componentWillMount:' + this.state.x);
this.setState({x: this.props.x + '(willMount)'});
}
componentDidMount() {
Scheduler.log('componentDidMount:' + this.state.x);
}
render() {
Scheduler.log('render:' + this.state.x);
return <span />;
}
}
function App(props) {
Scheduler.log('App');
return <LifeCycle x={props.x} />;
}
ReactNoop.render(<App x={0} />);
ReactNoop.flushDeferredPri(20);
assertLog(['App', 'componentWillMount:0(ctor)', 'render:0(willMount)']);
ReactNoop.render(<App x={1} />);
await waitForAll([
'App',
'componentWillMount:0(willMount)',
'render:1(willMount)',
'componentDidMount:1(willMount)',
]);
});
it.skip('calls componentWill* twice if an update render is aborted', async () => {
class LifeCycle extends React.Component {
UNSAFE_componentWillMount() {
Scheduler.log('componentWillMount:' + this.props.x);
}
componentDidMount() {
Scheduler.log('componentDidMount:' + this.props.x);
}
UNSAFE_componentWillReceiveProps(nextProps) {
Scheduler.log(
'componentWillReceiveProps:' + this.props.x + '-' + nextProps.x,
);
}
shouldComponentUpdate(nextProps) {
Scheduler.log(
'shouldComponentUpdate:' + this.props.x + '-' + nextProps.x,
);
return true;
}
UNSAFE_componentWillUpdate(nextProps) {
Scheduler.log(
'componentWillUpdate:' + this.props.x + '-' + nextProps.x,
);
}
componentDidUpdate(prevProps) {
Scheduler.log('componentDidUpdate:' + this.props.x + '-' + prevProps.x);
}
render() {
Scheduler.log('render:' + this.props.x);
return <span />;
}
}
function Sibling() {
Scheduler.log('Sibling');
return <span />;
}
function App(props) {
Scheduler.log('App');
return [<LifeCycle key="a" x={props.x} />, <Sibling key="b" />];
}
ReactNoop.render(<App x={0} />);
await waitForAll([
'App',
'componentWillMount:0',
'render:0',
'Sibling',
'componentDidMount:0',
]);
ReactNoop.render(<App x={1} />);
ReactNoop.flushDeferredPri(30);
assertLog([
'App',
'componentWillReceiveProps:0-1',
'shouldComponentUpdate:0-1',
'componentWillUpdate:0-1',
'render:1',
'Sibling',
]);
ReactNoop.render(<App x={2} />);
await waitForAll([
'App',
'componentWillReceiveProps:1-2',
'shouldComponentUpdate:1-2',
'componentWillUpdate:1-2',
'render:2',
'Sibling',
'componentDidUpdate:2-0',
]);
});
it('calls getDerivedStateFromProps even for state-only updates', async () => {
let instance;
class LifeCycle extends React.Component {
state = {};
static getDerivedStateFromProps(props, prevState) {
Scheduler.log('getDerivedStateFromProps');
return {foo: 'foo'};
}
changeState() {
this.setState({foo: 'bar'});
}
componentDidUpdate() {
Scheduler.log('componentDidUpdate');
}
render() {
Scheduler.log('render');
instance = this;
return null;
}
}
ReactNoop.render(<LifeCycle />);
await waitForAll(['getDerivedStateFromProps', 'render']);
expect(instance.state).toEqual({foo: 'foo'});
instance.changeState();
await waitForAll([
'getDerivedStateFromProps',
'render',
'componentDidUpdate',
]);
expect(instance.state).toEqual({foo: 'foo'});
});
it('does not call getDerivedStateFromProps if neither state nor props have changed', async () => {
class Parent extends React.Component {
state = {parentRenders: 0};
static getDerivedStateFromProps(props, prevState) {
Scheduler.log('getDerivedStateFromProps');
return prevState.parentRenders + 1;
}
render() {
Scheduler.log('Parent');
return <Child parentRenders={this.state.parentRenders} ref={child} />;
}
}
class Child extends React.Component {
render() {
Scheduler.log('Child');
return this.props.parentRenders;
}
}
const child = React.createRef(null);
ReactNoop.render(<Parent />);
await waitForAll(['getDerivedStateFromProps', 'Parent', 'Child']);
child.current.setState({});
await waitForAll(['Child']);
});
it.skip('does not call componentWillReceiveProps for state-only updates', async () => {
const instances = [];
class LifeCycle extends React.Component {
state = {x: 0};
tick() {
this.setState({
x: this.state.x + 1,
});
}
UNSAFE_componentWillMount() {
instances.push(this);
Scheduler.log('componentWillMount:' + this.state.x);
}
componentDidMount() {
Scheduler.log('componentDidMount:' + this.state.x);
}
UNSAFE_componentWillReceiveProps(nextProps) {
Scheduler.log('componentWillReceiveProps');
}
shouldComponentUpdate(nextProps, nextState) {
Scheduler.log(
'shouldComponentUpdate:' + this.state.x + '-' + nextState.x,
);
return true;
}
UNSAFE_componentWillUpdate(nextProps, nextState) {
Scheduler.log(
'componentWillUpdate:' + this.state.x + '-' + nextState.x,
);
}
componentDidUpdate(prevProps, prevState) {
Scheduler.log('componentDidUpdate:' + this.state.x + '-' + prevState.x);
}
render() {
Scheduler.log('render:' + this.state.x);
return <span />;
}
}
class Wrap extends React.Component {
state = {y: 0};
UNSAFE_componentWillMount() {
instances.push(this);
}
tick() {
this.setState({
y: this.state.y + 1,
});
}
render() {
Scheduler.log('Wrap');
return <LifeCycle y={this.state.y} />;
}
}
function Sibling() {
Scheduler.log('Sibling');
return <span />;
}
function App(props) {
Scheduler.log('App');
return [<Wrap key="a" />, <Sibling key="b" />];
}
ReactNoop.render(<App y={0} />);
await waitForAll([
'App',
'Wrap',
'componentWillMount:0',
'render:0',
'Sibling',
'componentDidMount:0',
]);
instances[1].tick();
ReactNoop.flushDeferredPri(25);
assertLog([
'shouldComponentUpdate:0-1',
'componentWillUpdate:0-1',
'render:1',
]);
instances[1].tick();
await waitForAll([
'shouldComponentUpdate:1-2',
'componentWillUpdate:1-2',
'render:2',
'componentDidUpdate:2-0',
]);
instances[0].tick();
ReactNoop.flushDeferredPri(30);
assertLog([
'Wrap',
'componentWillReceiveProps',
'shouldComponentUpdate:2-2',
'componentWillUpdate:2-2',
'render:2',
]);
instances[1].tick();
await waitForAll([
'shouldComponentUpdate:2-3',
'componentWillUpdate:2-3',
'render:3',
'componentDidUpdate:3-2',
]);
});
it.skip('skips will/DidUpdate when bailing unless an update was already in progress', async () => {
class LifeCycle extends React.Component {
UNSAFE_componentWillMount() {
Scheduler.log('componentWillMount');
}
componentDidMount() {
Scheduler.log('componentDidMount');
}
UNSAFE_componentWillReceiveProps(nextProps) {
Scheduler.log('componentWillReceiveProps');
}
shouldComponentUpdate(nextProps) {
Scheduler.log('shouldComponentUpdate');
return this.props.x !== nextProps.x;
}
UNSAFE_componentWillUpdate(nextProps) {
Scheduler.log('componentWillUpdate');
}
componentDidUpdate(prevProps) {
Scheduler.log('componentDidUpdate');
}
render() {
Scheduler.log('render');
return <span />;
}
}
function Sibling() {
Scheduler.log('render sibling');
return <span />;
}
function App(props) {
return [<LifeCycle key="a" x={props.x} />, <Sibling key="b" />];
}
ReactNoop.render(<App x={0} />);
await waitForAll([
'componentWillMount',
'render',
'render sibling',
'componentDidMount',
]);
ReactNoop.render(<App x={0} />);
await waitForAll([
'componentWillReceiveProps',
'shouldComponentUpdate',
'render sibling',
]);
ReactNoop.render(<App x={1} />);
ReactNoop.flushDeferredPri(30);
assertLog([
'componentWillReceiveProps',
'shouldComponentUpdate',
'componentWillUpdate',
'render',
'render sibling',
]);
ReactNoop.render(<App x={1} />);
await waitForAll([]);
assertLog([
'componentWillReceiveProps',
'shouldComponentUpdate',
'render sibling',
'componentDidUpdate',
]);
});
it('can nest batchedUpdates', async () => {
let instance;
class Foo extends React.Component {
state = {n: 0};
render() {
instance = this;
return <div />;
}
}
ReactNoop.render(<Foo />);
await waitForAll([]);
ReactNoop.flushSync(() => {
ReactNoop.batchedUpdates(() => {
instance.setState({n: 1}, () => Scheduler.log('setState 1'));
instance.setState({n: 2}, () => Scheduler.log('setState 2'));
ReactNoop.batchedUpdates(() => {
instance.setState({n: 3}, () => Scheduler.log('setState 3'));
instance.setState({n: 4}, () => Scheduler.log('setState 4'));
Scheduler.log('end inner batchedUpdates');
});
Scheduler.log('end outer batchedUpdates');
});
});
assertLog([
'end inner batchedUpdates',
'end outer batchedUpdates',
'setState 1',
'setState 2',
'setState 3',
'setState 4',
]);
expect(instance.state.n).toEqual(4);
});
it('can handle if setState callback throws', async () => {
let instance;
class Foo extends React.Component {
state = {n: 0};
render() {
instance = this;
return <div />;
}
}
ReactNoop.render(<Foo />);
await waitForAll([]);
function updater({n}) {
return {n: n + 1};
}
instance.setState(updater, () => Scheduler.log('first callback'));
instance.setState(updater, () => {
Scheduler.log('second callback');
throw new Error('callback error');
});
instance.setState(updater, () => Scheduler.log('third callback'));
await waitForThrow('callback error');
assertLog(['first callback', 'second callback']);
expect(instance.state.n).toEqual(3);
});
it('merges and masks context', async () => {
class Intl extends React.Component {
static childContextTypes = {
locale: PropTypes.string,
};
getChildContext() {
return {
locale: this.props.locale,
};
}
render() {
Scheduler.log('Intl ' + JSON.stringify(this.context));
return this.props.children;
}
}
class Router extends React.Component {
static childContextTypes = {
route: PropTypes.string,
};
getChildContext() {
return {
route: this.props.route,
};
}
render() {
Scheduler.log('Router ' + JSON.stringify(this.context));
return this.props.children;
}
}
class ShowLocale extends React.Component {
static contextTypes = {
locale: PropTypes.string,
};
render() {
Scheduler.log('ShowLocale ' + JSON.stringify(this.context));
return this.context.locale;
}
}
class ShowRoute extends React.Component {
static contextTypes = {
route: PropTypes.string,
};
render() {
Scheduler.log('ShowRoute ' + JSON.stringify(this.context));
return this.context.route;
}
}
function ShowBoth(props, context) {
Scheduler.log('ShowBoth ' + JSON.stringify(context));
return `${context.route} in ${context.locale}`;
}
ShowBoth.contextTypes = {
locale: PropTypes.string,
route: PropTypes.string,
};
class ShowNeither extends React.Component {
render() {
Scheduler.log('ShowNeither ' + JSON.stringify(this.context));
return null;
}
}
class Indirection extends React.Component {
render() {
Scheduler.log('Indirection ' + JSON.stringify(this.context));
return [
<ShowLocale key="a" />,
<ShowRoute key="b" />,
<ShowNeither key="c" />,
<Intl key="d" locale="ru">
<ShowBoth />
</Intl>,
<ShowBoth key="e" />,
];
}
}
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>,
);
await waitForAll([
'Intl {}',
'ShowLocale {"locale":"fr"}',
'ShowBoth {"locale":"fr"}',
]);
assertConsoleErrorDev([
'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'ShowLocale uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'ShowBoth uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
]);
ReactNoop.render(
<Intl locale="de">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>,
);
await waitForAll([
'Intl {}',
'ShowLocale {"locale":"de"}',
'ShowBoth {"locale":"de"}',
]);
React.startTransition(() => {
ReactNoop.render(
<Intl locale="sv">
<ShowLocale />
<div>
<ShowBoth />
</div>
</Intl>,
);
});
await waitFor(['Intl {}']);
ReactNoop.render(
<Intl locale="en">
<ShowLocale />
<Router route="/about">
<Indirection />
</Router>
<ShowBoth />
</Intl>,
);
await waitForAll([
'ShowLocale {"locale":"sv"}',
'ShowBoth {"locale":"sv"}',
'Intl {}',
'ShowLocale {"locale":"en"}',
'Router {}',
'Indirection {}',
'ShowLocale {"locale":"en"}',
'ShowRoute {"route":"/about"}',
'ShowNeither {}',
'Intl {}',
'ShowBoth {"locale":"ru","route":"/about"}',
'ShowBoth {"locale":"en","route":"/about"}',
'ShowBoth {"locale":"en"}',
]);
assertConsoleErrorDev([
'Router uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'ShowRoute uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
});
it('does not leak own context into context provider', async () => {
if (gate(flags => flags.disableLegacyContext)) {
throw new Error('This test infinite loops when context is disabled.');
}
class Recurse extends React.Component {
static contextTypes = {
n: PropTypes.number,
};
static childContextTypes = {
n: PropTypes.number,
};
getChildContext() {
return {n: (this.context.n || 3) - 1};
}
render() {
Scheduler.log('Recurse ' + JSON.stringify(this.context));
if (this.context.n === 0) {
return null;
}
return <Recurse />;
}
}
ReactNoop.render(<Recurse />);
await waitForAll([
'Recurse {}',
'Recurse {"n":2}',
'Recurse {"n":1}',
'Recurse {"n":0}',
]);
assertConsoleErrorDev([
'Recurse uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'Recurse uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
});
it('provides context when reusing work', async () => {
class Intl extends React.Component {
static childContextTypes = {
locale: PropTypes.string,
};
getChildContext() {
return {
locale: this.props.locale,
};
}
render() {
Scheduler.log('Intl ' + JSON.stringify(this.context));
return this.props.children;
}
}
class ShowLocale extends React.Component {
static contextTypes = {
locale: PropTypes.string,
};
render() {
Scheduler.log('ShowLocale ' + JSON.stringify(this.context));
return this.context.locale;
}
}
React.startTransition(() => {
ReactNoop.render(
<Intl locale="fr">
<ShowLocale />
<LegacyHiddenDiv mode="hidden">
<ShowLocale />
<Intl locale="ru">
<ShowLocale />
</Intl>
</LegacyHiddenDiv>
<ShowLocale />
</Intl>,
);
});
await waitFor([
'Intl {}',
'ShowLocale {"locale":"fr"}',
'ShowLocale {"locale":"fr"}',
]);
assertConsoleErrorDev([
'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'ShowLocale uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
await waitForAll([
'ShowLocale {"locale":"fr"}',
'Intl {}',
'ShowLocale {"locale":"ru"}',
]);
});
it('reads context when setState is below the provider', async () => {
let statefulInst;
class Intl extends React.Component {
static childContextTypes = {
locale: PropTypes.string,
};
getChildContext() {
const childContext = {
locale: this.props.locale,
};
Scheduler.log('Intl:provide ' + JSON.stringify(childContext));
return childContext;
}
render() {
Scheduler.log('Intl:read ' + JSON.stringify(this.context));
return this.props.children;
}
}
class ShowLocaleClass extends React.Component {
static contextTypes = {
locale: PropTypes.string,
};
render() {
Scheduler.log('ShowLocaleClass:read ' + JSON.stringify(this.context));
return this.context.locale;
}
}
function ShowLocaleFn(props, context) {
Scheduler.log('ShowLocaleFn:read ' + JSON.stringify(context));
return context.locale;
}
ShowLocaleFn.contextTypes = {
locale: PropTypes.string,
};
class Stateful extends React.Component {
state = {x: 0};
render() {
statefulInst = this;
return this.props.children;
}
}
function IndirectionFn(props, context) {
Scheduler.log('IndirectionFn ' + JSON.stringify(context));
return props.children;
}
class IndirectionClass extends React.Component {
render() {
Scheduler.log('IndirectionClass ' + JSON.stringify(this.context));
return this.props.children;
}
}
ReactNoop.render(
<Intl locale="fr">
<IndirectionFn>
<IndirectionClass>
<Stateful>
<ShowLocaleClass />
<ShowLocaleFn />
</Stateful>
</IndirectionClass>
</IndirectionFn>
</Intl>,
);
await waitForAll([
'Intl:read {}',
'Intl:provide {"locale":"fr"}',
'IndirectionFn {}',
'IndirectionClass {}',
'ShowLocaleClass:read {"locale":"fr"}',
'ShowLocaleFn:read {"locale":"fr"}',
]);
assertConsoleErrorDev([
'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
]);
statefulInst.setState({x: 1});
await waitForAll([]);
assertLog([]);
});
it('reads context when setState is above the provider', async () => {
let statefulInst;
class Intl extends React.Component {
static childContextTypes = {
locale: PropTypes.string,
};
getChildContext() {
const childContext = {
locale: this.props.locale,
};
Scheduler.log('Intl:provide ' + JSON.stringify(childContext));
return childContext;
}
render() {
Scheduler.log('Intl:read ' + JSON.stringify(this.context));
return this.props.children;
}
}
class ShowLocaleClass extends React.Component {
static contextTypes = {
locale: PropTypes.string,
};
render() {
Scheduler.log('ShowLocaleClass:read ' + JSON.stringify(this.context));
return this.context.locale;
}
}
function ShowLocaleFn(props, context) {
Scheduler.log('ShowLocaleFn:read ' + JSON.stringify(context));
return context.locale;
}
ShowLocaleFn.contextTypes = {
locale: PropTypes.string,
};
function IndirectionFn(props, context) {
Scheduler.log('IndirectionFn ' + JSON.stringify(context));
return props.children;
}
class IndirectionClass extends React.Component {
render() {
Scheduler.log('IndirectionClass ' + JSON.stringify(this.context));
return this.props.children;
}
}
class Stateful extends React.Component {
state = {locale: 'fr'};
render() {
statefulInst = this;
return <Intl locale={this.state.locale}>{this.props.children}</Intl>;
}
}
ReactNoop.render(
<Stateful>
<IndirectionFn>
<IndirectionClass>
<ShowLocaleClass />
<ShowLocaleFn />
</IndirectionClass>
</IndirectionFn>
</Stateful>,
);
await waitForAll([
'Intl:read {}',
'Intl:provide {"locale":"fr"}',
'IndirectionFn {}',
'IndirectionClass {}',
'ShowLocaleClass:read {"locale":"fr"}',
'ShowLocaleFn:read {"locale":"fr"}',
]);
assertConsoleErrorDev([
'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.',
]);
statefulInst.setState({locale: 'gr'});
await waitForAll([
'Intl:read {}',
'Intl:provide {"locale":"gr"}',
'IndirectionFn {}',
'IndirectionClass {}',
'ShowLocaleClass:read {"locale":"gr"}',
'ShowLocaleFn:read {"locale":"gr"}',
]);
});
it('maintains the correct context when providers bail out due to low priority', async () => {
class Root extends React.Component {
render() {
return <Middle {...this.props} />;
}
}
let instance;
class Middle extends React.Component {
constructor(props, context) {
super(props, context);
instance = this;
}
shouldComponentUpdate() {
return false;
}
render() {
return <Child />;
}
}
class Child extends React.Component {
static childContextTypes = {};
getChildContext() {
return {};
}
render() {
return <div />;
}
}
ReactNoop.render(<Root />);
await waitForAll([]);
assertConsoleErrorDev([
'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
]);
instance.setState({});
await waitForAll([]);
});
it('maintains the correct context when unwinding due to an error in render', async () => {
class Root extends React.Component {
componentDidCatch(error) {
}
render() {
return <ContextProvider depth={1} />;
}
}
let instance;
class ContextProvider extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {};
if (props.depth === 1) {
instance = this;
}
}
static childContextTypes = {};
getChildContext() {
return {};
}
render() {
if (this.state.throwError) {
throw Error();
}
return this.props.depth < 4 ? (
<ContextProvider depth={this.props.depth + 1} />
) : (
<div />
);
}
}
ReactNoop.render(<Root />);
await expect(async () => await waitForAll([])).toErrorDev([
'ContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
]);
instance.setState({
throwError: true,
});
await expect(async () => await waitForAll([])).toErrorDev(
'Error boundaries should implement getDerivedStateFromError()',
);
});
it('should not recreate masked context unless inputs have changed', async () => {
let scuCounter = 0;
class MyComponent extends React.Component {
static contextTypes = {};
componentDidMount(prevProps, prevState) {
Scheduler.log('componentDidMount');
this.setState({setStateInCDU: true});
}
componentDidUpdate(prevProps, prevState) {
Scheduler.log('componentDidUpdate');
if (this.state.setStateInCDU) {
this.setState({setStateInCDU: false});
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
Scheduler.log('componentWillReceiveProps');
this.setState({setStateInCDU: true});
}
render() {
Scheduler.log('render');
return null;
}
shouldComponentUpdate(nextProps, nextState) {
Scheduler.log('shouldComponentUpdate');
return scuCounter++ < 5;
}
}
ReactNoop.render(<MyComponent />);
await waitForAll([
'render',
'componentDidMount',
'shouldComponentUpdate',
'render',
'componentDidUpdate',
'shouldComponentUpdate',
'render',
'componentDidUpdate',
]);
assertConsoleErrorDev([
'MyComponent uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
});
it.skip('should reuse memoized work if pointers are updated before calling lifecycles', async () => {
const cduNextProps = [];
const cduPrevProps = [];
const scuNextProps = [];
const scuPrevProps = [];
let renderCounter = 0;
function SecondChild(props) {
return <span>{props.children}</span>;
}
class FirstChild extends React.Component {
componentDidUpdate(prevProps, prevState) {
cduNextProps.push(this.props);
cduPrevProps.push(prevProps);
}
shouldComponentUpdate(nextProps, nextState) {
scuNextProps.push(nextProps);
scuPrevProps.push(this.props);
return this.props.children !== nextProps.children;
}
render() {
renderCounter++;
return <span>{this.props.children}</span>;
}
}
class Middle extends React.Component {
render() {
return (
<div>
<FirstChild>{this.props.children}</FirstChild>
<SecondChild>{this.props.children}</SecondChild>
</div>
);
}
}
function Root(props) {
return (
<div hidden={true}>
<Middle {...props} />
</div>
);
}
ReactNoop.render(<Root>A</Root>);
await waitForAll([]);
expect(renderCounter).toBe(1);
ReactNoop.render(<Root>B</Root>);
ReactNoop.flushDeferredPri(20 + 30 + 5);
expect(renderCounter).toBe(2);
expect(scuPrevProps).toEqual([{children: 'A'}]);
expect(scuNextProps).toEqual([{children: 'B'}]);
expect(cduPrevProps).toEqual([]);
expect(cduNextProps).toEqual([]);
ReactNoop.render(<Root>B</Root>);
await waitForAll([]);
expect(renderCounter).toBe(2);
expect(scuPrevProps).toEqual([{children: 'A'}, {children: 'B'}]);
expect(scuNextProps).toEqual([{children: 'B'}, {children: 'B'}]);
expect(cduPrevProps).toEqual([{children: 'A'}]);
expect(cduNextProps).toEqual([{children: 'B'}]);
});
it('updates descendants with new context values', async () => {
let instance;
class TopContextProvider extends React.Component {
static childContextTypes = {
count: PropTypes.number,
};
constructor() {
super();
this.state = {count: 0};
instance = this;
}
getChildContext = () => ({
count: this.state.count,
});
render = () => this.props.children;
updateCount = () =>
this.setState(state => ({
count: state.count + 1,
}));
}
class Middle extends React.Component {
render = () => this.props.children;
}
class Child extends React.Component {
static contextTypes = {
count: PropTypes.number,
};
render = () => {
Scheduler.log(`count:${this.context.count}`);
return null;
};
}
ReactNoop.render(
<TopContextProvider>
<Middle>
<Child />
</Middle>
</TopContextProvider>,
);
await waitForAll(['count:0']);
assertConsoleErrorDev([
'TopContextProvider 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.',
]);
instance.updateCount();
await waitForAll(['count:1']);
});
it('updates descendants with multiple context-providing ancestors with new context values', async () => {
let instance;
class TopContextProvider extends React.Component {
static childContextTypes = {
count: PropTypes.number,
};
constructor() {
super();
this.state = {count: 0};
instance = this;
}
getChildContext = () => ({
count: this.state.count,
});
render = () => this.props.children;
updateCount = () =>
this.setState(state => ({
count: state.count + 1,
}));
}
class MiddleContextProvider extends React.Component {
static childContextTypes = {
name: PropTypes.string,
};
getChildContext = () => ({
name: 'brian',
});
render = () => this.props.children;
}
class Child extends React.Component {
static contextTypes = {
count: PropTypes.number,
};
render = () => {
Scheduler.log(`count:${this.context.count}`);
return null;
};
}
ReactNoop.render(
<TopContextProvider>
<MiddleContextProvider>
<Child />
</MiddleContextProvider>
</TopContextProvider>,
);
await waitForAll(['count:0']);
assertConsoleErrorDev([
'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'MiddleContextProvider 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.',
]);
instance.updateCount();
await waitForAll(['count:1']);
});
it('should not update descendants with new context values if shouldComponentUpdate returns false', async () => {
let instance;
class TopContextProvider extends React.Component {
static childContextTypes = {
count: PropTypes.number,
};
constructor() {
super();
this.state = {count: 0};
instance = this;
}
getChildContext = () => ({
count: this.state.count,
});
render = () => this.props.children;
updateCount = () =>
this.setState(state => ({
count: state.count + 1,
}));
}
class MiddleScu extends React.Component {
shouldComponentUpdate() {
return false;
}
render = () => this.props.children;
}
class MiddleContextProvider extends React.Component {
static childContextTypes = {
name: PropTypes.string,
};
getChildContext = () => ({
name: 'brian',
});
render = () => this.props.children;
}
class Child extends React.Component {
static contextTypes = {
count: PropTypes.number,
};
render = () => {
Scheduler.log(`count:${this.context.count}`);
return null;
};
}
ReactNoop.render(
<TopContextProvider>
<MiddleScu>
<MiddleContextProvider>
<Child />
</MiddleContextProvider>
</MiddleScu>
</TopContextProvider>,
);
await waitForAll(['count:0']);
assertConsoleErrorDev([
'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'MiddleContextProvider 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.',
]);
instance.updateCount();
await waitForAll([]);
});
it('should update descendants with new context values if setState() is called in the middle of the tree', async () => {
let middleInstance;
let topInstance;
class TopContextProvider extends React.Component {
static childContextTypes = {
count: PropTypes.number,
};
constructor() {
super();
this.state = {count: 0};
topInstance = this;
}
getChildContext = () => ({
count: this.state.count,
});
render = () => this.props.children;
updateCount = () =>
this.setState(state => ({
count: state.count + 1,
}));
}
class MiddleScu extends React.Component {
shouldComponentUpdate() {
return false;
}
render = () => this.props.children;
}
class MiddleContextProvider extends React.Component {
static childContextTypes = {
name: PropTypes.string,
};
constructor() {
super();
this.state = {name: 'brian'};
middleInstance = this;
}
getChildContext = () => ({
name: this.state.name,
});
updateName = name => {
this.setState({name});
};
render = () => this.props.children;
}
class Child extends React.Component {
static contextTypes = {
count: PropTypes.number,
name: PropTypes.string,
};
render = () => {
Scheduler.log(`count:${this.context.count}, name:${this.context.name}`);
return null;
};
}
ReactNoop.render(
<TopContextProvider>
<MiddleScu>
<MiddleContextProvider>
<Child />
</MiddleContextProvider>
</MiddleScu>
</TopContextProvider>,
);
await waitForAll(['count:0, name:brian']);
assertConsoleErrorDev([
'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
'MiddleContextProvider 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.',
]);
topInstance.updateCount();
await waitForAll([]);
middleInstance.updateName('not brian');
await waitForAll(['count:1, name:not brian']);
});
it('does not interrupt for update at same priority', async () => {
function Parent(props) {
Scheduler.log('Parent: ' + props.step);
return <Child step={props.step} />;
}
function Child(props) {
Scheduler.log('Child: ' + props.step);
return null;
}
React.startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
await waitFor(['Parent: 1']);
ReactNoop.render(<Parent step={2} />);
await waitForAll(['Child: 1', 'Parent: 2', 'Child: 2']);
});
it('does not interrupt for update at lower priority', async () => {
function Parent(props) {
Scheduler.log('Parent: ' + props.step);
return <Child step={props.step} />;
}
function Child(props) {
Scheduler.log('Child: ' + props.step);
return null;
}
React.startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
await waitFor(['Parent: 1']);
ReactNoop.expire(2000);
ReactNoop.render(<Parent step={2} />);
await waitForAll(['Child: 1', 'Parent: 2', 'Child: 2']);
});
it('does interrupt for update at higher priority', async () => {
function Parent(props) {
Scheduler.log('Parent: ' + props.step);
return <Child step={props.step} />;
}
function Child(props) {
Scheduler.log('Child: ' + props.step);
return null;
}
React.startTransition(() => {
ReactNoop.render(<Parent step={1} />);
});
await waitFor(['Parent: 1']);
ReactNoop.flushSync(() => ReactNoop.render(<Parent step={2} />));
assertLog(['Parent: 2', 'Child: 2']);
await waitForAll([]);
});
it('does not break with a bad Map polyfill', async () => {
const realMapSet = Map.prototype.set;
async function triggerCodePathThatUsesFibersAsMapKeys() {
function Thing() {
throw new Error('No.');
}
class Boundary extends React.Component {
state = {didError: false};
componentDidCatch() {
this.setState({didError: true});
}
static contextTypes = {
color: () => null,
};
render() {
return this.state.didError ? null : <Thing />;
}
}
ReactNoop.render(
<React.StrictMode>
<Boundary />
</React.StrictMode>,
);
await expect(async () => {
await waitForAll([]);
}).toErrorDev([
'Boundary uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
'Legacy context API has been detected within a strict-mode tree',
]);
}
jest.resetModules();
let receivedNonExtensibleObjects;
Map.prototype.set = function (key) {
if (typeof key === 'object' && key !== null) {
if (!Object.isExtensible(key)) {
receivedNonExtensibleObjects = true;
}
}
return realMapSet.apply(this, arguments);
};
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
let InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
waitForThrow = InternalTestUtils.waitForThrow;
assertLog = InternalTestUtils.assertLog;
try {
receivedNonExtensibleObjects = false;
await triggerCodePathThatUsesFibersAsMapKeys();
} finally {
Map.prototype.set = realMapSet;
}
expect(receivedNonExtensibleObjects).toBe(__DEV__);
jest.resetModules();
Map.prototype.set = function (key, value) {
if (typeof key === 'object' && key !== null) {
key.__internalValueSlot = value;
}
return realMapSet.apply(this, arguments);
};
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
waitForThrow = InternalTestUtils.waitForThrow;
assertLog = InternalTestUtils.assertLog;
try {
await triggerCodePathThatUsesFibersAsMapKeys();
} finally {
Map.prototype.set = realMapSet;
}
});
});