UNPKG

react-native

Version:

A framework for building native apps using React

2,049 lines (1,722 loc) • 56.3 kB
/** * Copyright 2013-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @emails react-core */ 'use strict'; var React; var ReactNoop; var ReactFeatureFlags; describe('ReactIncremental', () => { beforeEach(() => { jest.resetModules(); React = require('React'); ReactNoop = require('ReactNoop'); ReactFeatureFlags = require('ReactFeatureFlags'); ReactFeatureFlags.disableNewFiberFeatures = false; }); it('should render a simple component', () => { function Bar() { return <div>Hello World</div>; } function Foo() { return <Bar isBar={true} />; } ReactNoop.render(<Foo />); ReactNoop.flush(); }); it('should render a simple component, in steps if needed', () => { var renderCallbackCalled = false; var barCalled = false; function Bar() { barCalled = true; return <span><div>Hello World</div></span>; } var fooCalled = false; function Foo() { fooCalled = true; return [ <Bar isBar={true} />, <Bar isBar={true} />, ]; } ReactNoop.render(<Foo />, () => renderCallbackCalled = true); expect(fooCalled).toBe(false); expect(barCalled).toBe(false); expect(renderCallbackCalled).toBe(false); // Do one step of work. ReactNoop.flushDeferredPri(7 + 5); expect(fooCalled).toBe(true); expect(barCalled).toBe(false); expect(renderCallbackCalled).toBe(false); // Do the rest of the work. ReactNoop.flushDeferredPri(50); expect(fooCalled).toBe(true); expect(barCalled).toBe(true); expect(renderCallbackCalled).toBe(true); }); it('updates a previous render', () => { var ops = []; function Header() { ops.push('Header'); return <h1>Hi</h1>; } function Content(props) { ops.push('Content'); return <div>{props.children}</div>; } function Footer() { ops.push('Footer'); return <footer>Bye</footer>; } var header = <Header />; var footer = <Footer />; function Foo(props) { ops.push('Foo'); return ( <div> {header} <Content>{props.text}</Content> {footer} </div> ); } ReactNoop.render(<Foo text="foo" />, () => ops.push('renderCallbackCalled')); ReactNoop.flush(); expect(ops).toEqual(['Foo', 'Header', 'Content', 'Footer', 'renderCallbackCalled']); ops = []; ReactNoop.render(<Foo text="bar" />, () => ops.push('firstRenderCallbackCalled')); ReactNoop.render(<Foo text="bar" />, () => ops.push('secondRenderCallbackCalled')); ReactNoop.flush(); // TODO: Test bail out of host components. This is currently unobservable. // Since this is an update, it should bail out and reuse the work from // Header and Content. expect(ops).toEqual(['Foo', 'Content', 'firstRenderCallbackCalled', 'secondRenderCallbackCalled']); }); it('can cancel partially rendered work and restart', () => { var ops = []; function Bar(props) { ops.push('Bar'); return <div>{props.children}</div>; } function Foo(props) { ops.push('Foo'); return ( <div> <Bar>{props.text}</Bar> <Bar>{props.text}</Bar> </div> ); } // Init ReactNoop.render(<Foo text="foo" />); ReactNoop.flush(); ops = []; ReactNoop.render(<Foo text="bar" />); // Flush part of the work ReactNoop.flushDeferredPri(20 + 5); expect(ops).toEqual(['Foo', 'Bar']); ops = []; // This will abort the previous work and restart ReactNoop.render(<Foo text="baz" />); // Flush part of the new work ReactNoop.flushDeferredPri(20 + 5); expect(ops).toEqual(['Foo', 'Bar']); // Flush the rest of the work which now includes the low priority ReactNoop.flush(20); expect(ops).toEqual(['Foo', 'Bar', 'Bar']); }); it('can deprioritize unfinished work and resume it later', () => { var ops = []; function Bar(props) { ops.push('Bar'); return <div>{props.children}</div>; } function Middle(props) { ops.push('Middle'); return <span>{props.children}</span>; } function Foo(props) { ops.push('Foo'); return ( <div> <Bar>{props.text}</Bar> <section hidden={true}> <Middle>{props.text}</Middle> </section> <Bar>{props.text}</Bar> <footer hidden={true}> <Middle>Footer</Middle> </footer> </div> ); } // Init ReactNoop.render(<Foo text="foo" />); ReactNoop.flush(); expect(ops).toEqual(['Foo', 'Bar', 'Bar', 'Middle', 'Middle']); ops = []; // Render part of the work. This should be enough to flush everything except // the middle which has lower priority. ReactNoop.render(<Foo text="bar" />); ReactNoop.flushDeferredPri(40); expect(ops).toEqual(['Foo', 'Bar', 'Bar']); ops = []; // Flush only the remaining work ReactNoop.flush(); expect(ops).toEqual(['Middle', 'Middle']); }); it('can deprioritize a tree from without dropping work', () => { var ops = []; function Bar(props) { ops.push('Bar'); return <div>{props.children}</div>; } function Middle(props) { ops.push('Middle'); return <span>{props.children}</span>; } function Foo(props) { ops.push('Foo'); return ( <div> <Bar>{props.text}</Bar> <section hidden={true}> <Middle>{props.text}</Middle> </section> <Bar>{props.text}</Bar> <footer hidden={true}> <Middle>Footer</Middle> </footer> </div> ); } // Init ReactNoop.performAnimationWork(() => { ReactNoop.render(<Foo text="foo" />); }); ReactNoop.flush(); expect(ops).toEqual(['Foo', 'Bar', 'Bar', 'Middle', 'Middle']); ops = []; // Render the high priority work (everying except the hidden trees). ReactNoop.performAnimationWork(() => { ReactNoop.render(<Foo text="foo" />); }); ReactNoop.flushAnimationPri(); expect(ops).toEqual(['Foo', 'Bar', 'Bar']); ops = []; // The hidden content was deprioritized from high to low priority. A low // priority callback should have been scheduled. Flush it now. ReactNoop.flushDeferredPri(); expect(ops).toEqual(['Middle', 'Middle']); }); it('can resume work in a subtree even when a parent bails out', () => { var ops = []; function Bar(props) { ops.push('Bar'); return <div>{props.children}</div>; } function Tester() { // This component is just here to ensure that the bail out is // in fact in effect in the expected place for this test. ops.push('Tester'); return <div />; } function Middle(props) { ops.push('Middle'); return <span>{props.children}</span>; } var middleContent = ( <aaa> <Tester /> <bbb hidden={true}> <ccc> <Middle>Hi</Middle> </ccc> </bbb> </aaa> ); function Foo(props) { ops.push('Foo'); return ( <div> <Bar>{props.text}</Bar> {middleContent} <Bar>{props.text}</Bar> </div> ); } // Init ReactNoop.render(<Foo text="foo" />); ReactNoop.flushDeferredPri(52); expect(ops).toEqual(['Foo', 'Bar', 'Tester', 'Bar']); ops = []; // We're now rendering an update that will bail out on updating middle. ReactNoop.render(<Foo text="bar" />); ReactNoop.flushDeferredPri(45 + 5); expect(ops).toEqual(['Foo', 'Bar', 'Bar']); ops = []; // Flush the rest to make sure that the bailout didn't block this work. ReactNoop.flush(); expect(ops).toEqual(['Middle']); }); it('can resume work in a bailed subtree within one pass', () => { var ops = []; function Bar(props) { ops.push('Bar'); return <div>{props.children}</div>; } class Tester extends React.Component { shouldComponentUpdate() { return false; } render() { // This component is just here to ensure that the bail out is // in fact in effect in the expected place for this test. ops.push('Tester'); return <div />; } } function Middle(props) { ops.push('Middle'); return <span>{props.children}</span>; } // Should content not just bail out on current, not workInProgress? class Content extends React.Component { shouldComponentUpdate() { return false; } render() { return [ <Tester unused={this.props.unused} />, <bbb hidden={true}> <ccc> <Middle>Hi</Middle> </ccc> </bbb>, ]; } } function Foo(props) { ops.push('Foo'); return ( <div hidden={props.text === 'bar'}> <Bar>{props.text}</Bar> <Content unused={props.text} /> <Bar>{props.text}</Bar> </div> ); } // Init ReactNoop.render(<Foo text="foo" />); ReactNoop.flushDeferredPri(52 + 5); expect(ops).toEqual(['Foo', 'Bar', 'Tester', 'Bar']); ops = []; // Make a quick update which will create a low pri tree on top of the // already low pri tree. ReactNoop.render(<Foo text="bar" />); ReactNoop.flushDeferredPri(15); expect(ops).toEqual(['Foo']); ops = []; // At this point, middle will bail out but it has not yet fully rendered. // Since that is the same priority as its parent tree. This should render // as a single batch. Therefore, it is correct that Middle should be in the // middle. If it occurs after the two "Bar" components then it was flushed // after them which is not correct. ReactNoop.flush(); expect(ops).toEqual(['Bar', 'Middle', 'Bar']); ops = []; // Let us try this again without fully finishing the first time. This will // create a hanging subtree that is reconciling at the normal priority. ReactNoop.render(<Foo text="foo" />); ReactNoop.flushDeferredPri(40); expect(ops).toEqual(['Foo', 'Bar']); ops = []; // This update will create a tree that aborts that work and down-prioritizes // it. If the priority levels aren't down-prioritized correctly this may // abort rendering of the down-prioritized content. ReactNoop.render(<Foo text="bar" />); ReactNoop.flush(); expect(ops).toEqual(['Foo', 'Bar', 'Bar']); }); it('can reuse work done after being preempted', () => { var ops = []; function Bar(props) { ops.push('Bar'); return <div>{props.children}</div>; } function Middle(props) { ops.push('Middle'); return <span>{props.children}</span>; } var middleContent = ( <div> <Middle>Hello</Middle> <Bar>-</Bar> <Middle>World</Middle> </div> ); var step0 = ( <div> <Middle>Hi</Middle> <Bar>{'Foo'}</Bar> <Middle>There</Middle> </div> ); function Foo(props) { ops.push('Foo'); return ( <div> <Bar>{props.text2}</Bar> <div hidden={true}> { props.step === 0 ? step0 : middleContent } </div> </div> ); } // Init ReactNoop.render(<Foo text="foo" text2="foo" step={0} />); ReactNoop.flushDeferredPri(55 + 25 + 5 + 5); // We only finish the higher priority work. So the low pri content // has not yet finished mounting. expect(ops).toEqual(['Foo', 'Bar', 'Middle', 'Bar']); ops = []; // Interrupt the rendering with a quick update. This should not touch the // middle content. ReactNoop.render(<Foo text="foo" text2="bar" step={0} />); ReactNoop.flush(); // We've now rendered the entire tree but we didn't have to redo the work // done by the first Middle and Bar already. expect(ops).toEqual(['Foo', 'Bar', 'Middle']); ops = []; // Make a quick update which will schedule low priority work to // update the middle content. ReactNoop.render(<Foo text="bar" text2="bar" step={1} />); ReactNoop.flushDeferredPri(30 + 25 + 5); expect(ops).toEqual(['Foo', 'Bar']); ops = []; // The middle content is now pending rendering... ReactNoop.flushDeferredPri(30 + 5); expect(ops).toEqual(['Middle', 'Bar']); ops = []; // but we'll interrupt it to render some higher priority work. // The middle content will bailout so it remains untouched. ReactNoop.render(<Foo text="foo" text2="bar" step={1} />); ReactNoop.flushDeferredPri(30); expect(ops).toEqual(['Foo', 'Bar']); ops = []; // Since we did nothing to the middle subtree during the interuption, // we should be able to reuse the reconciliation work that we already did // without restarting. ReactNoop.flush(); expect(ops).toEqual(['Middle']); }); it('can reuse work that began but did not complete, after being preempted', () => { let ops = []; let child; let sibling; function GreatGrandchild() { ops.push('GreatGrandchild'); return <div />; } function Grandchild() { ops.push('Grandchild'); return <GreatGrandchild />; } class Child extends React.Component { state = { step: 0 }; render() { child = this; ops.push('Child'); return <Grandchild />; } } class Sibling extends React.Component { render() { ops.push('Sibling'); sibling = this; return <div />; } } function Parent() { ops.push('Parent'); return [ // The extra div is necessary because when Parent bails out during the // high priority update, its progressedPriority is set to high. // So its direct children cannot be reused when we resume at // low priority. I think this would be fixed by changing // pendingWorkPriority and progressedPriority to be the priority of // the children only, not including the fiber itself. <div><Child /></div>, <Sibling />, ]; } ReactNoop.render(<Parent />); ReactNoop.flush(); ops = []; // Begin working on a low priority update to Child, but stop before // GreatGrandchild. Child and Grandchild begin but don't complete. child.setState({ step: 1 }); ReactNoop.flushDeferredPri(30); expect(ops).toEqual([ 'Child', 'Grandchild', ]); // Interrupt the current low pri work with a high pri update elsewhere in // the tree. ops = []; ReactNoop.performAnimationWork(() => { sibling.setState({}); }); ReactNoop.flushAnimationPri(); expect(ops).toEqual(['Sibling']); // Continue the low pri work. The work on Child and GrandChild was memoized // so they should not be worked on again. ops = []; ReactNoop.flush(); expect(ops).toEqual([ // No Child // No Grandchild 'GreatGrandchild', ]); }); it('can reuse work if shouldComponentUpdate is false, after being preempted', () => { var ops = []; function Bar(props) { ops.push('Bar'); return <div>{props.children}</div>; } class Middle extends React.Component { shouldComponentUpdate(nextProps) { return this.props.children !== nextProps.children; } render() { ops.push('Middle'); return <span>{this.props.children}</span>; } } class Content extends React.Component { shouldComponentUpdate(nextProps) { return this.props.step !== nextProps.step; } render() { ops.push('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) { ops.push('Foo'); return ( <div> <Bar>{props.text}</Bar> <div hidden={true}> <Content step={props.step} text={props.text} /> </div> </div> ); } // Init ReactNoop.render(<Foo text="foo" step={0} />); ReactNoop.flush(); expect(ops).toEqual(['Foo', 'Bar', 'Content', 'Middle', 'Bar', 'Middle']); ops = []; // Make a quick update which will schedule low priority work to // update the middle content. ReactNoop.render(<Foo text="bar" step={1} />); ReactNoop.flushDeferredPri(30 + 5); expect(ops).toEqual(['Foo', 'Bar']); ops = []; // The middle content is now pending rendering... ReactNoop.flushDeferredPri(30 + 25 + 5); expect(ops).toEqual(['Content', 'Middle', 'Bar']); // One more Middle left. ops = []; // but we'll interrupt it to render some higher priority work. // The middle content will bailout so it remains untouched. ReactNoop.render(<Foo text="foo" step={1} />); ReactNoop.flushDeferredPri(30); expect(ops).toEqual(['Foo', 'Bar']); ops = []; // Since we did nothing to the middle subtree during the interuption, // we should be able to reuse the reconciliation work that we already did // without restarting. ReactNoop.flush(); expect(ops).toEqual(['Middle']); }); it('memoizes work even if shouldComponentUpdate returns false', () => { let ops = []; class Foo extends React.Component { shouldComponentUpdate(nextProps) { // this.props is the memoized props. So this should return true for // every update except the first one. const shouldUpdate = this.props.step !== 1; ops.push('shouldComponentUpdate: ' + shouldUpdate); return shouldUpdate; } render() { ops.push('render'); return <div />; } } ReactNoop.render(<Foo step={1} />); ReactNoop.flush(); ops = []; ReactNoop.render(<Foo step={2} />); ReactNoop.flush(); expect(ops).toEqual([ 'shouldComponentUpdate: false', ]); ops = []; ReactNoop.render(<Foo step={3} />); ReactNoop.flush(); expect(ops).toEqual([ // If the memoized props were not updated during last bail out, sCU will // keep returning false. 'shouldComponentUpdate: true', 'render', ]); }); it('can update in the middle of a tree using setState', () => { 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 />); ReactNoop.flush(); expect(instance.state).toEqual({ a: 'a' }); instance.setState({ b: 'b' }); ReactNoop.flush(); expect(instance.state).toEqual({ a: 'a', b: 'b' }); }); it('can queue multiple state updates', () => { 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 />); ReactNoop.flush(); // Call setState multiple times before flushing instance.setState({ b: 'b' }); instance.setState({ c: 'c' }); instance.setState({ d: 'd' }); ReactNoop.flush(); expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c', d: 'd' }); }); it('can use updater form of setState', () => { 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} />); ReactNoop.flush(); expect(instance.state.num).toEqual(1); instance.setState(updater); ReactNoop.flush(); expect(instance.state.num).toEqual(2); instance.setState(updater); ReactNoop.render(<Foo multiplier={3} />); ReactNoop.flush(); expect(instance.state.num).toEqual(6); }); it('can call setState inside update callback', () => { 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} />); ReactNoop.flush(); instance.setState(updater); instance.setState(updater, callback); ReactNoop.flush(); expect(instance.state.num).toEqual(4); expect(instance.state.called).toEqual(true); }); it('can replaceState', () => { let instance; const Bar = React.createClass({ getInitialState() { instance = this; return { a: 'a' }; }, render() { return <div>{this.props.children}</div>; }, }); function Foo() { return ( <div> <Bar /> </div> ); } ReactNoop.render(<Foo />); ReactNoop.flush(); instance.setState({ b: 'b' }); instance.setState({ c: 'c' }); instance.replaceState({ d: 'd' }); ReactNoop.flush(); expect(instance.state).toEqual({ d: 'd' }); }); it('can forceUpdate', () => { const ops = []; function Baz() { ops.push('Baz'); return <div />; } let instance; class Bar extends React.Component { constructor() { super(); instance = this; } shouldComponentUpdate() { return false; } render() { ops.push('Bar'); return <Baz />; } } function Foo() { ops.push('Foo'); return ( <div> <Bar /> </div> ); } ReactNoop.render(<Foo />); ReactNoop.flush(); expect(ops).toEqual(['Foo', 'Bar', 'Baz']); instance.forceUpdate(); ReactNoop.flush(); expect(ops).toEqual(['Foo', 'Bar', 'Baz', 'Bar', 'Baz']); }); it('can call sCU while resuming a partly mounted component', () => { var ops = []; var 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() { ops.push('Bar:' + this.props.x); return <span prop={'' + (this.props.x === this.state.y)} />; } } function Foo(props) { ops.push('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); expect(ops).toEqual(['Foo', 'Bar:A', 'Bar:B', 'Bar:C']); expect(instances.size).toBe(3); ops = []; ReactNoop.render(<Foo step={1} />); ReactNoop.flushDeferredPri(50); // A was memoized and reused. B was memoized but couldn't be reused because // props differences. C was memoized and reused. D never even started so it // needed a new instance. expect(ops).toEqual(['Foo', 'Bar:B2', 'Bar:D']); // We expect each rerender to correspond to a new instance. expect(instances.size).toBe(5); }); it('gets new props when setting state on a partly updated component', () => { var ops = []; var instances = []; class Bar extends React.Component { state = { y: 'A' }; constructor() { super(); instances.push(this); } performAction() { this.setState({ y: 'B', }); } render() { ops.push('Bar:' + this.props.x + '-' + this.props.step); return <span prop={'' + (this.props.x === this.state.y)} />; } } function Baz() { // This component is used as a sibling to Foo so that we can fully // complete Foo, without committing. ops.push('Baz'); return <div />; } function Foo(props) { ops.push('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>); ReactNoop.flush(); ops = []; // Flush part way through with new props, fully completing the first Bar. // However, it doesn't commit yet. ReactNoop.render(<div><Foo step={1} /><Baz /><Baz /></div>); ReactNoop.flushDeferredPri(45); expect(ops).toEqual(['Foo', 'Bar:A-1', 'Bar:B-1', 'Baz']); // Make an update to the same Bar. instances[0].performAction(); ops = []; ReactNoop.flush(); expect(ops).toEqual(['Bar:A-1', 'Baz']); }); it('calls componentWillMount twice if the initial render is aborted', () => { var ops = []; class LifeCycle extends React.Component { state = { x: this.props.x }; componentWillMount() { ops.push('componentWillMount:' + this.state.x + '-' + this.props.x); } componentDidMount() { ops.push('componentDidMount:' + this.state.x + '-' + this.props.x); } render() { return <span />; } } function Trail() { ops.push('Trail'); return null; } function App(props) { ops.push('App'); return ( <div> <LifeCycle x={props.x} /> <Trail /> </div> ); } ReactNoop.render(<App x={0} />); ReactNoop.flushDeferredPri(30); expect(ops).toEqual([ 'App', 'componentWillMount:0-0', ]); ops = []; ReactNoop.render(<App x={1} />); ReactNoop.flush(); expect(ops).toEqual([ 'App', 'componentWillMount:1-1', 'Trail', 'componentDidMount:1-1', ]); }); it('uses state set in componentWillMount even if initial render was aborted', () => { var ops = []; class LifeCycle extends React.Component { constructor(props) { super(props); this.state = {x: this.props.x + '(ctor)'}; } componentWillMount() { ops.push('componentWillMount:' + this.state.x); this.setState({x: this.props.x + '(willMount)'}); } componentDidMount() { ops.push('componentDidMount:' + this.state.x); } render() { ops.push('render:' + this.state.x); return <span />; } } function App(props) { ops.push('App'); return <LifeCycle x={props.x} />; } ReactNoop.render(<App x={0} />); ReactNoop.flushDeferredPri(20); expect(ops).toEqual([ 'App', 'componentWillMount:0(ctor)', 'render:0(willMount)', ]); ops = []; ReactNoop.render(<App x={1} />); ReactNoop.flush(); expect(ops).toEqual([ 'App', 'componentWillMount:1(ctor)', 'render:1(willMount)', 'componentDidMount:1(willMount)', ]); }); it('calls componentWill* twice if an update render is aborted', () => { var ops = []; class LifeCycle extends React.Component { componentWillMount() { ops.push('componentWillMount:' + this.props.x); } componentDidMount() { ops.push('componentDidMount:' + this.props.x); } componentWillReceiveProps(nextProps) { ops.push('componentWillReceiveProps:' + this.props.x + '-' + nextProps.x); } shouldComponentUpdate(nextProps) { ops.push('shouldComponentUpdate:' + this.props.x + '-' + nextProps.x); return true; } componentWillUpdate(nextProps) { ops.push('componentWillUpdate:' + this.props.x + '-' + nextProps.x); } componentDidUpdate(prevProps) { ops.push('componentDidUpdate:' + this.props.x + '-' + prevProps.x); } render() { ops.push('render:' + this.props.x); return <span />; } } function Sibling() { // The sibling is used to confirm that we've completed the first child, // but not yet flushed. ops.push('Sibling'); return <span />; } function App(props) { ops.push('App'); return [ <LifeCycle key="a" x={props.x} />, <Sibling key="b" />, ]; } ReactNoop.render(<App x={0} />); ReactNoop.flush(); expect(ops).toEqual([ 'App', 'componentWillMount:0', 'render:0', 'Sibling', 'componentDidMount:0', ]); ops = []; ReactNoop.render(<App x={1} />); ReactNoop.flushDeferredPri(30); expect(ops).toEqual([ 'App', 'componentWillReceiveProps:0-1', 'shouldComponentUpdate:0-1', 'componentWillUpdate:0-1', 'render:1', 'Sibling', // no componentDidUpdate ]); ops = []; ReactNoop.render(<App x={2} />); ReactNoop.flush(); expect(ops).toEqual([ 'App', 'componentWillReceiveProps:1-2', 'shouldComponentUpdate:1-2', 'componentWillUpdate:1-2', 'render:2', 'Sibling', // When componentDidUpdate finally gets called, it covers both updates. 'componentDidUpdate:2-0', ]); }); it('does not call componentWillReceiveProps for state-only updates', () => { var ops = []; var instances = []; class LifeCycle extends React.Component { state = { x: 0 }; tick() { this.setState({ x: this.state.x + 1, }); } componentWillMount() { instances.push(this); ops.push('componentWillMount:' + this.state.x); } componentDidMount() { ops.push('componentDidMount:' + this.state.x); } componentWillReceiveProps(nextProps) { ops.push('componentWillReceiveProps'); } shouldComponentUpdate(nextProps, nextState) { ops.push('shouldComponentUpdate:' + this.state.x + '-' + nextState.x); return true; } componentWillUpdate(nextProps, nextState) { ops.push('componentWillUpdate:' + this.state.x + '-' + nextState.x); } componentDidUpdate(prevProps, prevState) { ops.push('componentDidUpdate:' + this.state.x + '-' + prevState.x); } render() { ops.push('render:' + this.state.x); return <span />; } } // This wrap is a bit contrived because we can't pause a completed root and // there is currently an issue where a component can't reuse its render // output unless it fully completed. class Wrap extends React.Component { state = { y: 0 }; componentWillMount() { instances.push(this); } tick() { this.setState({ y: this.state.y + 1, }); } render() { ops.push('Wrap'); return <LifeCycle y={this.state.y} />; } } function Sibling() { // The sibling is used to confirm that we've completed the first child, // but not yet flushed. ops.push('Sibling'); return <span />; } function App(props) { ops.push('App'); return [ <Wrap key="a" />, <Sibling key="b" />, ]; } ReactNoop.render(<App y={0} />); ReactNoop.flush(); expect(ops).toEqual([ 'App', 'Wrap', 'componentWillMount:0', 'render:0', 'Sibling', 'componentDidMount:0', ]); ops = []; // LifeCycle instances[1].tick(); ReactNoop.flushDeferredPri(25); expect(ops).toEqual([ // no componentWillReceiveProps 'shouldComponentUpdate:0-1', 'componentWillUpdate:0-1', 'render:1', // no componentDidUpdate ]); ops = []; // LifeCycle instances[1].tick(); ReactNoop.flush(); expect(ops).toEqual([ // no componentWillReceiveProps 'shouldComponentUpdate:1-2', 'componentWillUpdate:1-2', 'render:2', // When componentDidUpdate finally gets called, it covers both updates. 'componentDidUpdate:2-0', ]); ops = []; // Next we will update props of LifeCycle by updating its parent. instances[0].tick(); ReactNoop.flushDeferredPri(30); expect(ops).toEqual([ 'Wrap', 'componentWillReceiveProps', 'shouldComponentUpdate:2-2', 'componentWillUpdate:2-2', 'render:2', // no componentDidUpdate ]); ops = []; // Next we will update LifeCycle directly but not with new props. instances[1].tick(); ReactNoop.flush(); expect(ops).toEqual([ // This should not trigger another componentWillReceiveProps because // we never got new props. 'shouldComponentUpdate:2-3', 'componentWillUpdate:2-3', 'render:3', 'componentDidUpdate:3-2', ]); // TODO: Test that we get the expected values for the same scenario with // incomplete parents. }); it('skips will/DidUpdate when bailing unless an update was already in progress', () => { var ops = []; class LifeCycle extends React.Component { componentWillMount() { ops.push('componentWillMount'); } componentDidMount() { ops.push('componentDidMount'); } componentWillReceiveProps(nextProps) { ops.push('componentWillReceiveProps'); } shouldComponentUpdate(nextProps) { ops.push('shouldComponentUpdate'); // Bail return this.props.x !== nextProps.x; } componentWillUpdate(nextProps) { ops.push('componentWillUpdate'); } componentDidUpdate(prevProps) { ops.push('componentDidUpdate'); } render() { ops.push('render'); return <span />; } } function Sibling() { ops.push('render sibling'); return <span />; } function App(props) { return [ <LifeCycle x={props.x} />, <Sibling />, ]; } ReactNoop.render(<App x={0} />); ReactNoop.flush(); expect(ops).toEqual([ 'componentWillMount', 'render', 'render sibling', 'componentDidMount', ]); ops = []; // Update to same props ReactNoop.render(<App x={0} />); ReactNoop.flush(); expect(ops).toEqual([ 'componentWillReceiveProps', 'shouldComponentUpdate', // no componentWillUpdate // no render 'render sibling', // no componentDidUpdate ]); ops = []; // Begin updating to new props... ReactNoop.render(<App x={1} />); ReactNoop.flushDeferredPri(30); expect(ops).toEqual([ 'componentWillReceiveProps', 'shouldComponentUpdate', 'componentWillUpdate', 'render', 'render sibling', // no componentDidUpdate yet ]); ops = []; // ...but we'll interrupt it to rerender the same props. ReactNoop.render(<App x={1} />); ReactNoop.flush(); // We can bail out this time, but we must call componentDidUpdate. expect(ops).toEqual([ 'componentWillReceiveProps', 'shouldComponentUpdate', // no componentWillUpdate // no render 'render sibling', 'componentDidUpdate', ]); }); it('performs batched updates at the end of the batch', () => { var ops = []; var instance; class Foo extends React.Component { state = { n: 0 }; render() { instance = this; return <div />; } } ReactNoop.render(<Foo />); ReactNoop.flush(); ops = []; ReactNoop.syncUpdates(() => { ReactNoop.batchedUpdates(() => { instance.setState({ n: 1 }, () => ops.push('setState 1')); instance.setState({ n: 2 }, () => ops.push('setState 2')); ops.push('end batchedUpdates'); }); ops.push('end syncUpdates'); }); // ReactNoop.flush() not needed because updates are synchronous expect(ops).toEqual([ 'end batchedUpdates', 'setState 1', 'setState 2', 'end syncUpdates', ]); expect(instance.state.n).toEqual(2); }); it('can nest batchedUpdates', () => { var ops = []; var instance; class Foo extends React.Component { state = { n: 0 }; render() { instance = this; return <div />; } } ReactNoop.render(<Foo />); ReactNoop.flush(); ops = []; ReactNoop.syncUpdates(() => { ReactNoop.batchedUpdates(() => { instance.setState({ n: 1 }, () => ops.push('setState 1')); instance.setState({ n: 2 }, () => ops.push('setState 2')); ReactNoop.batchedUpdates(() => { instance.setState({ n: 3 }, () => ops.push('setState 3')); instance.setState({ n: 4 }, () => ops.push('setState 4')); ops.push('end inner batchedUpdates'); }); ops.push('end outer batchedUpdates'); }); ops.push('end syncUpdates'); }); // ReactNoop.flush() not needed because updates are synchronous expect(ops).toEqual([ 'end inner batchedUpdates', 'end outer batchedUpdates', 'setState 1', 'setState 2', 'setState 3', 'setState 4', 'end syncUpdates', ]); expect(instance.state.n).toEqual(4); }); it('can handle if setState callback throws', () => { var ops = []; var instance; class Foo extends React.Component { state = { n: 0 }; render() { instance = this; return <div />; } } ReactNoop.render(<Foo />); ReactNoop.flush(); ops = []; function updater({ n }) { return { n: n + 1 }; } instance.setState(updater, () => ops.push('first callback')); instance.setState(updater, () => { ops.push('second callback'); throw new Error('callback error'); }); instance.setState(updater, () => ops.push('third callback')); expect(() => { ReactNoop.flush(); }).toThrow('callback error'); // The third callback isn't called because the second one throws expect(ops).toEqual([ 'first callback', 'second callback', ]); expect(instance.state.n).toEqual(3); }); it('merges and masks context', () => { var ops = []; class Intl extends React.Component { static childContextTypes = { locale: React.PropTypes.string, }; getChildContext() { return { locale: this.props.locale, }; } render() { ops.push('Intl ' + JSON.stringify(this.context)); return this.props.children; } } class Router extends React.Component { static childContextTypes = { route: React.PropTypes.string, }; getChildContext() { return { route: this.props.route, }; } render() { ops.push('Router ' + JSON.stringify(this.context)); return this.props.children; } } class ShowLocale extends React.Component { static contextTypes = { locale: React.PropTypes.string, }; render() { ops.push('ShowLocale ' + JSON.stringify(this.context)); return this.context.locale; } } class ShowRoute extends React.Component { static contextTypes = { route: React.PropTypes.string, }; render() { ops.push('ShowRoute ' + JSON.stringify(this.context)); return this.context.route; } } function ShowBoth(props, context) { ops.push('ShowBoth ' + JSON.stringify(context)); return `${context.route} in ${context.locale}`; } ShowBoth.contextTypes = { locale: React.PropTypes.string, route: React.PropTypes.string, }; class ShowNeither extends React.Component { render() { ops.push('ShowNeither ' + JSON.stringify(this.context)); return null; } } class Indirection extends React.Component { render() { ops.push('Indirection ' + JSON.stringify(this.context)); return [ <ShowLocale />, <ShowRoute />, <ShowNeither />, <Intl locale="ru"> <ShowBoth /> </Intl>, <ShowBoth />, ]; } } ops.length = 0; ReactNoop.render( <Intl locale="fr"> <ShowLocale /> <div> <ShowBoth /> </div> </Intl> ); ReactNoop.flush(); expect(ops).toEqual([ 'Intl {}', 'ShowLocale {"locale":"fr"}', 'ShowBoth {"locale":"fr"}', ]); ops.length = 0; ReactNoop.render( <Intl locale="de"> <ShowLocale /> <div> <ShowBoth /> </div> </Intl> ); ReactNoop.flush(); expect(ops).toEqual([ 'Intl {}', 'ShowLocale {"locale":"de"}', 'ShowBoth {"locale":"de"}', ]); ops.length = 0; ReactNoop.render( <Intl locale="sv"> <ShowLocale /> <div> <ShowBoth /> </div> </Intl> ); ReactNoop.flushDeferredPri(15); expect(ops).toEqual([ 'Intl {}', ]); ops.length = 0; ReactNoop.render( <Intl locale="en"> <ShowLocale /> <Router route="/about"> <Indirection /> </Router> <ShowBoth /> </Intl> ); ReactNoop.flush(); expect(ops).toEqual([ '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"}', ]); }); it('does not leak own context into context provider', () => { var ops = []; class Recurse extends React.Component { static contextTypes = { n: React.PropTypes.number, }; static childContextTypes = { n: React.PropTypes.number, }; getChildContext() { return {n: (this.context.n || 3) - 1}; } render() { ops.push('Recurse ' + JSON.stringify(this.context)); if (this.context.n === 0) { return null; } return <Recurse />; } } ReactNoop.render(<Recurse />); ReactNoop.flush(); expect(ops).toEqual([ 'Recurse {}', 'Recurse {"n":2}', 'Recurse {"n":1}', 'Recurse {"n":0}', ]); }); it('provides context when reusing work', () => { var ops = []; class Intl extends React.Component { static childContextTypes = { locale: React.PropTypes.string, }; getChildContext() { return { locale: this.props.locale, }; } render() { ops.push('Intl ' + JSON.stringify(this.context)); return this.props.children; } } class ShowLocale extends React.Component { static contextTypes = { locale: React.PropTypes.string, }; render() { ops.push('ShowLocale ' + JSON.stringify(this.context)); return this.context.locale; } } ops.length = 0; ReactNoop.render( <Intl locale="fr"> <ShowLocale /> <div hidden="true"> <ShowLocale /> <Intl locale="ru"> <ShowLocale /> </Intl> </div> <ShowLocale /> </Intl> ); ReactNoop.flushDeferredPri(40); expect(ops).toEqual([ 'Intl {}', 'ShowLocale {"locale":"fr"}', 'ShowLocale {"locale":"fr"}', ]); ops.length = 0; ReactNoop.flush(); expect(ops).toEqual([ 'ShowLocale {"locale":"fr"}', 'Intl {}', 'ShowLocale {"locale":"ru"}', ]); }); it('reads context when setState is below the provider', () => { var ops = []; var statefulInst; class Intl extends React.Component { static childContextTypes = { locale: React.PropTypes.string, }; getChildContext() { const childContext = { locale: this.props.locale, }; ops.push('Intl:provide ' + JSON.stringify(childContext)); return childContext; } render() { ops.push('Intl:read ' + JSON.stringify(this.context)); return this.props.children; } } class ShowLocaleClass extends React.Component { static contextTypes = { locale: React.PropTypes.string, }; render() { ops.push('ShowLocaleClass:read ' + JSON.stringify(this.context)); return this.context.locale; } } function ShowLocaleFn(props, context) { ops.push('ShowLocaleFn:read ' + JSON.stringify(context)); return context.locale; } ShowLocaleFn.contextTypes = { locale: React.PropTypes.string, }; class Stateful extends React.Component { state = {x: 0}; render() { statefulInst = this; return this.props.children; } } function IndirectionFn(props, context) { ops.push('IndirectionFn ' + JSON.stringify(context)); return props.children; } class IndirectionClass extends React.Component { render() { ops.push('IndirectionClass ' + JSON.stringify(this.context)); return this.props.children; } } ops.length = 0; ReactNoop.render( <Intl locale="fr"> <IndirectionFn> <IndirectionClass> <Stateful> <ShowLocaleClass /> <ShowLocaleFn /> </Stateful> </IndirectionClass> </IndirectionFn> </Intl> ); ReactNoop.flush(); expect(ops).toEqual([ 'Intl:read {}', 'Intl:provide {"locale":"fr"}', 'IndirectionFn {}', 'IndirectionClass {}', 'ShowLocaleClass:read {"locale":"fr"}', 'ShowLocaleFn:read {"locale":"fr"}', ]); ops.length = 0; statefulInst.setState({x: 1}); ReactNoop.flush(); // All work has been memoized because setState() // happened below the context and could not have affected it. expect(ops).toEqual([]); }); it('reads context when setState is above the provider', () => { var ops = []; var statefulInst; class Intl extends React.Component { static childContextTypes = { locale: React.PropTypes.string, }; getChildContext() { const childContext = { locale: this.props.locale, }; ops.push('Intl:provide ' + JSON.stringify(childContext)); return childContext; } render() { ops.push('Intl:read ' + JSON.stringify(this.context)); return this.props.children; } } class ShowLocaleClass extends React.Component { static contextTypes = { locale: React.PropTypes.string, }; render() { ops.push('ShowLocaleClass:read ' + JSON.stringify(this.context)); return this.context.locale; } } function ShowLocaleFn(props, context) { ops.push('ShowLocaleFn:read ' + JSON.stringify(context)); return context.locale; } ShowLocaleFn.contextTypes = { locale: React.PropTypes.string, }; function IndirectionFn(props, context) { ops.push('IndirectionFn ' + JSON.stringify(context)); return props.children; } class IndirectionClass extends React.Component { render() { ops.push('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> ); } } ops.length = 0; ReactNoop.render( <Stateful> <IndirectionFn> <IndirectionClass> <ShowLocaleClass /> <ShowLocaleFn /> </IndirectionClass> </IndirectionFn> </Stateful> ); ReactNoop.flush(); expect(ops).toEqual([ 'Intl:read {}', 'Intl:provide {"locale":"fr"}', 'IndirectionFn {}', 'IndirectionClass {}', 'ShowLocaleClass:read {"locale":"fr"}', 'ShowLocaleFn:read {"locale":"fr"}', ]); ops.length = 0; statefulInst.setState({locale: 'gr'}); ReactNoop.flush(); expect(ops).toEqual([ // Intl is below setState() so it might have been // affected by it. Therefore we re-render and recompute // its child context. 'Intl:read {}', 'Intl:provide {"locale":"gr"}', // TODO: it's unfortunate that we can't reuse work on // these components even though they don't depend on context. 'IndirectionFn {}',