From ef96a5472b289feb76ae53a4f118a0869fab50ba Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 29 Dec 2016 18:09:45 -0800 Subject: [PATCH] Move memoization to begin phase Currently we update the memoized inputs (props, state) during the complete phase, as we go back up the tree. That means we can't reuse work until of its children have completed. By moving memoization to the begin phase, we can do a shallow bailout, reusing a unit of work even if there's still work to do in its children. Memoization now happens whenever a fiber's `child` property is updated; typically, right after reconciling. It's also updated when `shouldComponentUpdate` returns false, because that indicates that the given state and props are equal to the memoized state and props. Includes a test that confirms that work that is bailed out before completing can be reused without dropping the entire subtree. --- scripts/fiber/tests-failing.txt | 3 - scripts/fiber/tests-passing.txt | 3 + .../shared/fiber/ReactFiberBeginWork.js | 101 +++++++++----- .../shared/fiber/ReactFiberClassComponent.js | 72 ++++++++-- .../shared/fiber/ReactFiberCompleteWork.js | 31 +---- .../shared/fiber/ReactFiberScheduler.js | 4 - .../fiber/__tests__/ReactIncremental-test.js | 123 +++++++++++++++++- .../ReactIncrementalSideEffects-test.js | 4 +- 8 files changed, 255 insertions(+), 86 deletions(-) diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index ae1b00f2df5a36..75506fa455583e 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -1,6 +1,3 @@ -src/addons/__tests__/ReactComponentWithPureRenderMixin-test.js -* does not do a deep comparison - src/addons/__tests__/ReactFragment-test.js * should throw if a plain object is used as a child * should throw if a plain object even if it is in an owner diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index ab34e726913fad..3aa687f5e08e2c 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -36,6 +36,7 @@ scripts/error-codes/__tests__/invertObject-test.js src/addons/__tests__/ReactComponentWithPureRenderMixin-test.js * provides a default shouldComponentUpdate implementation +* does not do a deep comparison src/addons/__tests__/ReactFragment-test.js * warns for numeric keys on objects as children @@ -1141,7 +1142,9 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js * can resume work in a subtree even when a parent bails out * can resume work in a bailed subtree within one pass * can reuse work done after being preempted +* can reuse work that began but did not complete, after being preempted * can reuse work if shouldComponentUpdate is false, after being preempted +* memoizes work even if shouldComponentUpdate returns false * can update in the middle of a tree using setState * can queue multiple state updates * can use updater form of setState diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 128be59141b2b9..7bb84254adc11d 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -54,7 +54,6 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); var { - Update, Placement, ContentReset, Err, @@ -87,7 +86,12 @@ module.exports = function( mountClassInstance, resumeMountClassInstance, updateClassInstance, - } = ReactFiberClassComponent(scheduleUpdate, getPriorityContext); + } = ReactFiberClassComponent( + scheduleUpdate, + getPriorityContext, + memoizeProps, + memoizeState, + ); function markChildAsProgressed(current, workInProgress, priorityLevel) { // We now have clones. Let's store them as the currently progressed work. @@ -172,12 +176,13 @@ module.exports = function( // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. if (nextChildren === null) { - nextChildren = current && current.memoizedProps; + nextChildren = workInProgress.memoizedProps; } } else if (nextChildren === null || workInProgress.memoizedProps === nextChildren) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextChildren); return workInProgress.child; } @@ -198,16 +203,20 @@ module.exports = function( // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. if (nextProps === null) { - nextProps = current && current.memoizedProps; + nextProps = memoizedProps; + } + } else { + if (nextProps == null || memoizedProps === nextProps) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + // TODO: Disable this before release, since it is not part of the public API + // I use this for testing to compare the relative overhead of classes. + if (typeof fn.shouldComponentUpdate === 'function' && + !fn.shouldComponentUpdate(memoizedProps, nextProps)) { + // Memoize props even if shouldComponentUpdate returns false + memoizeProps(workInProgress, nextProps); + return bailoutOnAlreadyFinishedWork(current, workInProgress); } - } else if (nextProps === null || memoizedProps === nextProps || ( - // TODO: Disable this before release, since it is not part of the public API - // I use this for testing to compare the relative overhead of classes. - memoizedProps !== null && - typeof fn.shouldComponentUpdate === 'function' && - !fn.shouldComponentUpdate(memoizedProps, nextProps) - )) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); } var context = getMaskedContext(workInProgress); @@ -221,6 +230,7 @@ module.exports = function( nextChildren = fn(nextProps, context); } reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextProps); return workInProgress.child; } @@ -253,31 +263,23 @@ module.exports = function( shouldUpdate : boolean, hasContext : boolean, ) { - // Schedule side-effects - // Refs should update even if shouldComponentUpdate returns false markRef(current, workInProgress); - if (shouldUpdate) { - workInProgress.effectTag |= Update; - } else { - // If an update was already in progress, we should schedule an Update - // effect even though we're bailing out, so that cWU/cDU are called. - if (current) { - const instance = current.stateNode; - if (instance.props !== current.memoizedProps || - instance.state !== current.memoizedState) { - workInProgress.effectTag |= Update; - } - } + if (!shouldUpdate) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } - // Rerender const instance = workInProgress.stateNode; + + // Rerender ReactCurrentOwner.current = workInProgress; const nextChildren = instance.render(); reconcileChildren(current, workInProgress, nextChildren); + // Memoize props and state using the values we just used to render. + // TODO: Restructure so we never read values from the instance. + memoizeState(workInProgress, instance.state); + memoizeProps(workInProgress, instance.props); // The context might have changed so we need to recalculate it. if (hasContext) { @@ -316,7 +318,7 @@ module.exports = function( } const element = state.element; reconcileChildren(current, workInProgress, element); - workInProgress.memoizedState = state; + memoizeState(workInProgress, state); return workInProgress.child; } // If there is no update queue, that's a bailout because the root has no props. @@ -333,7 +335,7 @@ module.exports = function( // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. if (nextProps === null) { - nextProps = prevProps; + nextProps = memoizedProps; if (!nextProps) { throw new Error('We should always have pending or current props.'); } @@ -398,6 +400,7 @@ module.exports = function( // Reconcile the children and stash them for later work. reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority); + memoizeProps(workInProgress, nextProps); workInProgress.child = current ? current.child : null; if (!current) { @@ -417,10 +420,22 @@ module.exports = function( return null; } else { reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextProps); return workInProgress.child; } } + function updateHostText(current, workInProgress) { + let nextProps = workInProgress.pendingProps; + if (nextProps === null) { + nextProps = workInProgress.memoizedProps; + } + memoizeProps(workInProgress, nextProps); + // Nothing to do here. This is terminal. We'll do the completion step + // immediately after. + return null; + } + function mountIndeterminateComponent(current, workInProgress, priorityLevel) { if (current) { throw new Error('An indeterminate component should never have mounted.'); @@ -453,6 +468,7 @@ module.exports = function( // Proceed under the assumption that this is a functional component workInProgress.tag = FunctionalComponent; reconcileChildren(current, workInProgress, value); + memoizeProps(workInProgress, props); return workInProgress.child; } } @@ -472,6 +488,7 @@ module.exports = function( return bailoutOnAlreadyFinishedWork(current, workInProgress); } reconcileChildren(current, workInProgress, nextCoroutine.children); + memoizeProps(workInProgress, nextCoroutine); // This doesn't take arbitrary time so we could synchronously just begin // eagerly do the work of workInProgress.child as an optimization. return workInProgress.child; @@ -506,9 +523,11 @@ module.exports = function( nextChildren, priorityLevel ); + memoizeProps(workInProgress, nextChildren); markChildAsProgressed(current, workInProgress, priorityLevel); } else { reconcileChildren(current, workInProgress, nextChildren); + memoizeProps(workInProgress, nextChildren); } return workInProgress.child; } @@ -575,6 +594,18 @@ module.exports = function( return null; } + function memoizeProps(workInProgress : Fiber, nextProps : any) { + workInProgress.memoizedProps = nextProps; + // Reset the pending props + workInProgress.pendingProps = null; + } + + function memoizeState(workInProgress : Fiber, nextState : any) { + workInProgress.memoizedState = nextState; + // Don't reset the updateQueue, in case there are pending updates. Resetting + // is handled by beginUpdateQueue. + } + function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber { if (workInProgress.pendingWorkPriority === NoWork || workInProgress.pendingWorkPriority > priorityLevel) { @@ -608,9 +639,7 @@ module.exports = function( case HostComponent: return updateHostComponent(current, workInProgress); case HostText: - // Nothing to do here. This is terminal. We'll do the completion step - // immediately after. - return null; + return updateHostText(current, workInProgress); case CoroutineHandlerPhase: // This is a restart. Reset the tag to the initial phase. workInProgress.tag = CoroutineComponent; @@ -652,6 +681,14 @@ module.exports = function( // Unmount the current children as if the component rendered null const nextChildren = null; reconcileChildren(current, workInProgress, nextChildren); + + if (workInProgress.tag === ClassComponent) { + const instance = workInProgress.stateNode; + workInProgress.memoizedProps = instance.props; + workInProgress.memoizedState = instance.state; + workInProgress.pendingProps = null; + } + return workInProgress.child; } diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 99e9a08416a9c0..c229c4ff23fe83 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -15,6 +15,9 @@ import type { Fiber } from 'ReactFiber'; import type { PriorityLevel } from 'ReactPriorityLevel'; +var { + Update, +} = require('ReactTypeOfSideEffect'); var { getMaskedContext, } = require('ReactFiberContext'); @@ -36,6 +39,8 @@ const isArray = Array.isArray; module.exports = function( scheduleUpdate : (fiber : Fiber, priorityLevel : PriorityLevel) => void, getPriorityContext : () => PriorityLevel, + memoizeProps: (workInProgress : Fiber, props : any) => void, + memoizeState: (workInProgress : Fiber, state : any) => void, ) { // Class component state updater @@ -61,7 +66,7 @@ module.exports = function( }, }; - function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState, newContext) { + function checkShouldComponentUpdate(workInProgress, oldProps, newProps, oldState, newState, newContext) { if (oldProps === null || (workInProgress.updateQueue && workInProgress.updateQueue.hasForceUpdate)) { // If the workInProgress already has an Update effect, return true return true; @@ -87,7 +92,7 @@ module.exports = function( if (type.prototype && type.prototype.isPureReactComponent) { return ( !shallowEqual(oldProps, newProps) || - !shallowEqual(instance.state, newState) + !shallowEqual(oldState, newState) ); } @@ -194,6 +199,27 @@ module.exports = function( } } + + function markUpdate(workInProgress) { + workInProgress.effectTag |= Update; + } + + function markUpdateIfAlreadyInProgress(current: ?Fiber, workInProgress : Fiber) { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (current) { + if (workInProgress.memoizedProps !== current.memoizedProps || + workInProgress.memoizedState !== current.memoizedState) { + markUpdate(workInProgress); + } + } + } + + function resetInputPointers(workInProgress : Fiber, instance : any) { + instance.props = workInProgress.memoizedProps; + instance.state = workInProgress.memoizedState; + } + function adoptClassInstance(workInProgress : Fiber, instance : any) : void { instance.updater = updater; workInProgress.stateNode = instance; @@ -213,6 +239,7 @@ module.exports = function( // Invokes the mount life-cycles on a previously never rendered instance. function mountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : void { + markUpdate(workInProgress); const instance = workInProgress.stateNode; const state = instance.state || null; @@ -246,6 +273,10 @@ module.exports = function( // Called on a preexisting class instance. Returns false if a resumed render // could be reused. function resumeMountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean { + markUpdate(workInProgress); + const instance = workInProgress.stateNode; + resetInputPointers(workInProgress, instance); + let newState = workInProgress.memoizedState; let newProps = workInProgress.pendingProps; if (!newProps) { @@ -266,9 +297,15 @@ module.exports = function( workInProgress, workInProgress.memoizedProps, newProps, + workInProgress.memoizedState, newState, newContext )) { + // Update the existing instance's state, props, and context pointers even + // though we're bailing out. + instance.props = newProps; + instance.state = newState; + instance.context = newContext; return false; } @@ -302,8 +339,9 @@ module.exports = function( // Invokes the update life-cycles and returns false if it shouldn't rerender. function updateClassInstance(current : Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean { const instance = workInProgress.stateNode; + resetInputPointers(workInProgress, instance); - const oldProps = workInProgress.memoizedProps || current.memoizedProps; + const oldProps = workInProgress.memoizedProps; let newProps = workInProgress.pendingProps; if (!newProps) { // If there aren't any new props, then we'll reuse the memoized props. @@ -348,28 +386,40 @@ module.exports = function( oldState === newState && !hasContextChanged() && !(updateQueue && updateQueue.hasForceUpdate)) { + markUpdateIfAlreadyInProgress(current, workInProgress); return false; } - if (!checkShouldComponentUpdate( + const shouldUpdate = checkShouldComponentUpdate( workInProgress, oldProps, newProps, + oldState, newState, newContext - )) { - // TODO: Should this get the new props/state updated regardless? - return false; - } + ); - if (typeof instance.componentWillUpdate === 'function') { - instance.componentWillUpdate(newProps, newState, newContext); + if (shouldUpdate) { + markUpdate(workInProgress); + if (typeof instance.componentWillUpdate === 'function') { + instance.componentWillUpdate(newProps, newState, newContext); + } + } else { + markUpdateIfAlreadyInProgress(current, workInProgress); + + // If shouldComponentUpdate returned false, we should still update the + // memoized props/state to indicate that this work can be reused. + memoizeProps(workInProgress, newProps); + memoizeState(workInProgress, newState); } + // Update the existing instance's state, props, and context pointers even + // if shouldComponentUpdate returns false. instance.props = newProps; instance.state = newState; instance.context = newContext; - return true; + + return shouldUpdate; } return { diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 8c836cfda5d21c..cebef02a160ac9 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -100,7 +100,7 @@ module.exports = function( } function moveCoroutineToHandlerPhase(current : ?Fiber, workInProgress : Fiber) { - var coroutine = (workInProgress.pendingProps : ?ReactCoroutine); + var coroutine = (workInProgress.memoizedProps : ?ReactCoroutine); if (!coroutine) { throw new Error('Should be resolved by now'); } @@ -170,24 +170,14 @@ module.exports = function( switch (workInProgress.tag) { case FunctionalComponent: - workInProgress.memoizedProps = workInProgress.pendingProps; return null; case ClassComponent: { // We are leaving this subtree, so pop context if any. popContextProvider(workInProgress); - // Don't use the state queue to compute the memoized state. We already - // merged it and assigned it to the instance. Transfer it from there. - // Also need to transfer the props, because pendingProps will be null - // in the case of an update. - const instance = workInProgress.stateNode; - workInProgress.memoizedState = instance.state; - workInProgress.memoizedProps = instance.props; - return null; } case HostRoot: { // TODO: Pop the host container after #8607 lands. - workInProgress.memoizedProps = workInProgress.pendingProps; const fiberRoot = (workInProgress.stateNode : FiberRoot); if (fiberRoot.pendingContext) { fiberRoot.context = fiberRoot.pendingContext; @@ -198,7 +188,7 @@ module.exports = function( case HostComponent: popHostContext(workInProgress); const type = workInProgress.type; - let newProps = workInProgress.pendingProps; + const newProps = workInProgress.memoizedProps; if (current && workInProgress.stateNode != null) { // If we have an alternate, that means this is an update and we need to // schedule a side-effect to do the updates. @@ -207,9 +197,6 @@ module.exports = function( // have newProps so we'll have to reuse them. // TODO: Split the update API as separate for the props vs. children. // Even better would be if children weren't special cased at all tho. - if (!newProps) { - newProps = workInProgress.memoizedProps || oldProps; - } const instance : I = workInProgress.stateNode; const currentHostContext = getHostContext(); if (prepareUpdate(instance, type, oldProps, newProps, currentHostContext)) { @@ -255,20 +242,11 @@ module.exports = function( markUpdate(workInProgress); } } - workInProgress.memoizedProps = newProps; return null; case HostText: - let newText = workInProgress.pendingProps; + let newText = workInProgress.memoizedProps; if (current && workInProgress.stateNode != null) { const oldText = current.memoizedProps; - if (newText === null) { - // If this was a bail out we need to fall back to memoized text. - // This works the same way as HostComponent. - newText = workInProgress.memoizedProps; - if (newText === null) { - newText = oldText; - } - } // If we have an alternate, that means this is an update and we need // to schedule a side-effect to do the updates. if (oldText !== newText) { @@ -293,7 +271,6 @@ module.exports = function( case CoroutineComponent: return moveCoroutineToHandlerPhase(current, workInProgress); case CoroutineHandlerPhase: - workInProgress.memoizedProps = workInProgress.pendingProps; // Reset the tag to now be a first phase coroutine. workInProgress.tag = CoroutineComponent; return null; @@ -301,12 +278,10 @@ module.exports = function( // Does nothing. return null; case Fragment: - workInProgress.memoizedProps = workInProgress.pendingProps; return null; case HostPortal: // TODO: Only mark this as an update if we have any pending callbacks. markUpdate(workInProgress); - workInProgress.memoizedProps = workInProgress.pendingProps; popHostContainer(workInProgress); return null; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 435e3e6483ca06..51045b9f039ba6 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -442,10 +442,6 @@ module.exports = function(config : HostConfig { }); + 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
; + } + + function Grandchild() { + ops.push('Grandchild'); + return ; + } + + class Child extends React.Component { + state = { step: 0 }; + render() { + child = this; + ops.push('Child'); + return ; + } + } + + class Sibling extends React.Component { + render() { + ops.push('Sibling'); + sibling = this; + return
; + } + } + + 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. +
, + , + ]; + } + + ReactNoop.render(); + 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. + ops = []; + ReactNoop.flush(); + expect(ops).toEqual(['GreatGrandchild']); + }); + it('can reuse work if shouldComponentUpdate is false, after being preempted', () => { var ops = []; @@ -628,6 +702,43 @@ describe('ReactIncremental', () => { }); + 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
; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + + ops = []; + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([ + 'shouldComponentUpdate: false', + ]); + + ops = []; + ReactNoop.render(); + 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 { @@ -873,13 +984,13 @@ describe('ReactIncremental', () => { ReactNoop.render(); ReactNoop.flushDeferredPri(50); - // A completed and was reused. B completed but couldn't be reused because - // props differences. C didn't complete and therefore couldn't be reused. - // D never even started so it needed a new instance. - expect(ops).toEqual(['Foo', 'Bar:B2', 'Bar:C', 'Bar:D']); + // 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(6); + expect(instances.size).toBe(5); }); it('gets new props when setting state on a partly updated component', () => { @@ -935,7 +1046,7 @@ describe('ReactIncremental', () => { ops = []; ReactNoop.flush(); - expect(ops).toEqual(['Bar:A-1', 'Baz', 'Baz']); + expect(ops).toEqual(['Bar:A-1', 'Baz']); }); it('calls componentWillMount twice if the initial render is aborted', () => { diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js index 2666eecca7e506..441bcd8973e220 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -663,7 +663,7 @@ describe('ReactIncrementalSideEffects', () => { ), ]); - expect(ops).toEqual(['Foo', 'Baz', 'Bar']); + expect(ops).toEqual(['Foo']); ops = []; ReactNoop.flush(); @@ -857,7 +857,7 @@ describe('ReactIncrementalSideEffects', () => { ), ]); - expect(ops).toEqual(['Bar', 'Bar']); + expect(ops).toEqual(['Bar']); }); // TODO: Test that side-effects are not cut off when a work in progress node // moves to "current" without flushing due to having lower priority. Does this