@samitouri / QOS-React / commits / 1ed304fa87

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.

Andrew Clark committed Dec 29, 2016 at 18:09 UTC 1ed304fa877d2694597659d6d537c3d44dac322e
8 files changed +180 -86
scripts/fiber/tests-failing.txt
-3
@@ -1,6 +1,3 @@
1 -src/addons/__tests__/ReactComponentWithPureRenderMixin-test.js
2 -* does not do a deep comparison
3 -
1 src/addons/__tests__/ReactFragment-test.js
2 * should throw if a plain object is used as a child
3 * should throw if a plain object even if it is in an owner
scripts/fiber/tests-passing.txt
+2
@@ -36,6 +36,7 @@ scripts/error-codes/__tests__/invertObject-test.js
36
37 src/addons/__tests__/ReactComponentWithPureRenderMixin-test.js
38 * provides a default shouldComponentUpdate implementation
39 +* does not do a deep comparison
40
41 src/addons/__tests__/ReactFragment-test.js
42 * warns for numeric keys on objects as children
@@ -1143,6 +1144,7 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
1144 * can resume work in a bailed subtree within one pass
1145 * can reuse work done after being preempted
1146 * can reuse work if shouldComponentUpdate is false, after being preempted
1147 +* memoizes work even if shouldComponentUpdate returns false
1148 * can update in the middle of a tree using setState
1149 * can queue multiple state updates
1150 * can use updater form of setState
src/renderers/shared/fiber/ReactFiberBeginWork.js
+69 -32
@@ -55,7 +55,6 @@ var {
55 OffscreenPriority,
56 } = require('ReactPriorityLevel');
57 var {
58 - Update,
58 Placement,
59 ContentReset,
60 Err,
@@ -91,7 +90,12 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
90 mountClassInstance,
91 resumeMountClassInstance,
92 updateClassInstance,
94 - } = ReactFiberClassComponent(scheduleUpdate, getPriorityContext);
93 + } = ReactFiberClassComponent(
94 + scheduleUpdate,
95 + getPriorityContext,
96 + memoizeProps,
97 + memoizeState,
98 + );
99
100 function markChildAsProgressed(current, workInProgress, priorityLevel) {
101 // We now have clones. Let's store them as the currently progressed work.
@@ -176,12 +180,13 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
180 // Normally we can bail out on props equality but if context has changed
181 // we don't do the bailout and we have to reuse existing props instead.
182 if (nextChildren === null) {
179 - nextChildren = current && current.memoizedProps;
183 + nextChildren = workInProgress.memoizedProps;
184 }
185 } else if (nextChildren === null || workInProgress.memoizedProps === nextChildren) {
186 return bailoutOnAlreadyFinishedWork(current, workInProgress);
187 }
188 reconcileChildren(current, workInProgress, nextChildren);
189 + memoizeProps(workInProgress, nextChildren);
190 return workInProgress.child;
191 }
192
@@ -202,16 +207,20 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
207 // Normally we can bail out on props equality but if context has changed
208 // we don't do the bailout and we have to reuse existing props instead.
209 if (nextProps === null) {
205 - nextProps = current && current.memoizedProps;
210 + nextProps = memoizedProps;
211 + }
212 + } else {
213 + if (nextProps == null || memoizedProps === nextProps) {
214 + return bailoutOnAlreadyFinishedWork(current, workInProgress);
215 + }
216 + // TODO: Disable this before release, since it is not part of the public API
217 + // I use this for testing to compare the relative overhead of classes.
218 + if (typeof fn.shouldComponentUpdate === 'function' &&
219 + !fn.shouldComponentUpdate(memoizedProps, nextProps)) {
220 + // Memoize props even if shouldComponentUpdate returns false
221 + memoizeProps(workInProgress, nextProps);
222 + return bailoutOnAlreadyFinishedWork(current, workInProgress);
223 }
207 - } else if (nextProps === null || memoizedProps === nextProps || (
208 - // TODO: Disable this before release, since it is not part of the public API
209 - // I use this for testing to compare the relative overhead of classes.
210 - memoizedProps !== null &&
211 - typeof fn.shouldComponentUpdate === 'function' &&
212 - !fn.shouldComponentUpdate(memoizedProps, nextProps)
213 - )) {
214 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
224 }
225
226 var unmaskedContext = getUnmaskedContext(workInProgress);
@@ -226,6 +235,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
235 nextChildren = fn(nextProps, context);
236 }
237 reconcileChildren(current, workInProgress, nextChildren);
238 + memoizeProps(workInProgress, nextProps);
239 return workInProgress.child;
240 }
241
@@ -258,31 +268,23 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
268 shouldUpdate : boolean,
269 hasContext : boolean,
270 ) {
261 - // Schedule side-effects
262 -
271 // Refs should update even if shouldComponentUpdate returns false
272 markRef(current, workInProgress);
273
266 - if (shouldUpdate) {
267 - workInProgress.effectTag |= Update;
268 - } else {
269 - // If an update was already in progress, we should schedule an Update
270 - // effect even though we're bailing out, so that cWU/cDU are called.
271 - if (current) {
272 - const instance = current.stateNode;
273 - if (instance.props !== current.memoizedProps ||
274 - instance.state !== current.memoizedState) {
275 - workInProgress.effectTag |= Update;
276 - }
277 - }
274 + if (!shouldUpdate) {
275 return bailoutOnAlreadyFinishedWork(current, workInProgress);
276 }
277
281 - // Rerender
278 const instance = workInProgress.stateNode;
279 +
280 + // Rerender
281 ReactCurrentOwner.current = workInProgress;
282 const nextChildren = instance.render();
283 reconcileChildren(current, workInProgress, nextChildren);
284 + // Memoize props and state using the values we just used to render.
285 + // TODO: Restructure so we never read values from the instance.
286 + memoizeState(workInProgress, instance.state);
287 + memoizeProps(workInProgress, instance.props);
288
289 // The context might have changed so we need to recalculate it.
290 if (hasContext) {
@@ -321,7 +323,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
323 }
324 const element = state.element;
325 reconcileChildren(current, workInProgress, element);
324 - workInProgress.memoizedState = state;
326 + memoizeState(workInProgress, state);
327 return workInProgress.child;
328 }
329 // If there is no update queue, that's a bailout because the root has no props.
@@ -338,7 +340,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
340 // Normally we can bail out on props equality but if context has changed
341 // we don't do the bailout and we have to reuse existing props instead.
342 if (nextProps === null) {
341 - nextProps = prevProps;
343 + nextProps = memoizedProps;
344 if (!nextProps) {
345 throw new Error('We should always have pending or current props.');
346 }
@@ -403,6 +405,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
405
406 // Reconcile the children and stash them for later work.
407 reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority);
408 + memoizeProps(workInProgress, nextProps);
409 workInProgress.child = current ? current.child : null;
410
411 if (!current) {
@@ -422,10 +425,22 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
425 return null;
426 } else {
427 reconcileChildren(current, workInProgress, nextChildren);
428 + memoizeProps(workInProgress, nextProps);
429 return workInProgress.child;
430 }
431 }
432
433 + function updateHostText(current, workInProgress) {
434 + let nextProps = workInProgress.pendingProps;
435 + if (nextProps === null) {
436 + nextProps = workInProgress.memoizedProps;
437 + }
438 + memoizeProps(workInProgress, nextProps);
439 + // Nothing to do here. This is terminal. We'll do the completion step
440 + // immediately after.
441 + return null;
442 + }
443 +
444 function mountIndeterminateComponent(current, workInProgress, priorityLevel) {
445 if (current) {
446 throw new Error('An indeterminate component should never have mounted.');
@@ -484,6 +499,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
499 }
500 }
501 reconcileChildren(current, workInProgress, value);
502 + memoizeProps(workInProgress, props);
503 return workInProgress.child;
504 }
505 }
@@ -503,6 +519,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
519 return bailoutOnAlreadyFinishedWork(current, workInProgress);
520 }
521 reconcileChildren(current, workInProgress, nextCoroutine.children);
522 + memoizeProps(workInProgress, nextCoroutine);
523 // This doesn't take arbitrary time so we could synchronously just begin
524 // eagerly do the work of workInProgress.child as an optimization.
525 return workInProgress.child;
@@ -537,9 +554,11 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
554 nextChildren,
555 priorityLevel
556 );
557 + memoizeProps(workInProgress, nextChildren);
558 markChildAsProgressed(current, workInProgress, priorityLevel);
559 } else {
560 reconcileChildren(current, workInProgress, nextChildren);
561 + memoizeProps(workInProgress, nextChildren);
562 }
563 return workInProgress.child;
564 }
@@ -606,6 +625,18 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
625 return null;
626 }
627
628 + function memoizeProps(workInProgress : Fiber, nextProps : any) {
629 + workInProgress.memoizedProps = nextProps;
630 + // Reset the pending props
631 + workInProgress.pendingProps = null;
632 + }
633 +
634 + function memoizeState(workInProgress : Fiber, nextState : any) {
635 + workInProgress.memoizedState = nextState;
636 + // Don't reset the updateQueue, in case there are pending updates. Resetting
637 + // is handled by beginUpdateQueue.
638 + }
639 +
640 function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
641 if (workInProgress.pendingWorkPriority === NoWork ||
642 workInProgress.pendingWorkPriority > priorityLevel) {
@@ -639,9 +670,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
670 case HostComponent:
671 return updateHostComponent(current, workInProgress);
672 case HostText:
642 - // Nothing to do here. This is terminal. We'll do the completion step
643 - // immediately after.
644 - return null;
673 + return updateHostText(current, workInProgress);
674 case CoroutineHandlerPhase:
675 // This is a restart. Reset the tag to the initial phase.
676 workInProgress.tag = CoroutineComponent;
@@ -683,6 +712,14 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
712 // Unmount the current children as if the component rendered null
713 const nextChildren = null;
714 reconcileChildren(current, workInProgress, nextChildren);
715 +
716 + if (workInProgress.tag === ClassComponent) {
717 + const instance = workInProgress.stateNode;
718 + workInProgress.memoizedProps = instance.props;
719 + workInProgress.memoizedState = instance.state;
720 + workInProgress.pendingProps = null;
721 + }
722 +
723 return workInProgress.child;
724 }
725
src/renderers/shared/fiber/ReactFiberClassComponent.js
+61 -11
@@ -15,6 +15,9 @@
15 import type { Fiber } from 'ReactFiber';
16 import type { PriorityLevel } from 'ReactPriorityLevel';
17
18 +var {
19 + Update,
20 +} = require('ReactTypeOfSideEffect');
21 var {
22 cacheContext,
23 getMaskedContext,
@@ -40,6 +43,8 @@ const isArray = Array.isArray;
43 module.exports = function(
44 scheduleUpdate : (fiber : Fiber, priorityLevel : PriorityLevel) => void,
45 getPriorityContext : () => PriorityLevel,
46 + memoizeProps: (workInProgress : Fiber, props : any) => void,
47 + memoizeState: (workInProgress : Fiber, state : any) => void,
48 ) {
49
50 // Class component state updater
@@ -65,7 +70,7 @@ module.exports = function(
70 },
71 };
72
68 - function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState, newContext) {
73 + function checkShouldComponentUpdate(workInProgress, oldProps, newProps, oldState, newState, newContext) {
74 if (oldProps === null || (workInProgress.updateQueue && workInProgress.updateQueue.hasForceUpdate)) {
75 // If the workInProgress already has an Update effect, return true
76 return true;
@@ -91,7 +96,7 @@ module.exports = function(
96 if (type.prototype && type.prototype.isPureReactComponent) {
97 return (
98 !shallowEqual(oldProps, newProps) ||
94 - !shallowEqual(instance.state, newState)
99 + !shallowEqual(oldState, newState)
100 );
101 }
102
@@ -198,6 +203,27 @@ module.exports = function(
203 }
204 }
205
206 +
207 + function markUpdate(workInProgress) {
208 + workInProgress.effectTag |= Update;
209 + }
210 +
211 + function markUpdateIfAlreadyInProgress(current: ?Fiber, workInProgress : Fiber) {
212 + // If an update was already in progress, we should schedule an Update
213 + // effect even though we're bailing out, so that cWU/cDU are called.
214 + if (current) {
215 + if (workInProgress.memoizedProps !== current.memoizedProps ||
216 + workInProgress.memoizedState !== current.memoizedState) {
217 + markUpdate(workInProgress);
218 + }
219 + }
220 + }
221 +
222 + function resetInputPointers(workInProgress : Fiber, instance : any) {
223 + instance.props = workInProgress.memoizedProps;
224 + instance.state = workInProgress.memoizedState;
225 + }
226 +
227 function adoptClassInstance(workInProgress : Fiber, instance : any) : void {
228 instance.updater = updater;
229 workInProgress.stateNode = instance;
@@ -226,6 +252,7 @@ module.exports = function(
252
253 // Invokes the mount life-cycles on a previously never rendered instance.
254 function mountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : void {
255 + markUpdate(workInProgress);
256 const instance = workInProgress.stateNode;
257 const state = instance.state || null;
258
@@ -261,6 +288,10 @@ module.exports = function(
288 // Called on a preexisting class instance. Returns false if a resumed render
289 // could be reused.
290 function resumeMountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean {
291 + markUpdate(workInProgress);
292 + const instance = workInProgress.stateNode;
293 + resetInputPointers(workInProgress, instance);
294 +
295 let newState = workInProgress.memoizedState;
296 let newProps = workInProgress.pendingProps;
297 if (!newProps) {
@@ -282,9 +313,15 @@ module.exports = function(
313 workInProgress,
314 workInProgress.memoizedProps,
315 newProps,
316 + workInProgress.memoizedState,
317 newState,
318 newContext
319 )) {
320 + // Update the existing instance's state, props, and context pointers even
321 + // though we're bailing out.
322 + instance.props = newProps;
323 + instance.state = newState;
324 + instance.context = newContext;
325 return false;
326 }
327
@@ -318,8 +355,9 @@ module.exports = function(
355 // Invokes the update life-cycles and returns false if it shouldn't rerender.
356 function updateClassInstance(current : Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean {
357 const instance = workInProgress.stateNode;
358 + resetInputPointers(workInProgress, instance);
359
322 - const oldProps = workInProgress.memoizedProps || current.memoizedProps;
360 + const oldProps = workInProgress.memoizedProps;
361 let newProps = workInProgress.pendingProps;
362 if (!newProps) {
363 // If there aren't any new props, then we'll reuse the memoized props.
@@ -365,28 +403,40 @@ module.exports = function(
403 oldState === newState &&
404 !hasContextChanged() &&
405 !(updateQueue && updateQueue.hasForceUpdate)) {
406 + markUpdateIfAlreadyInProgress(current, workInProgress);
407 return false;
408 }
409
371 - if (!checkShouldComponentUpdate(
410 + const shouldUpdate = checkShouldComponentUpdate(
411 workInProgress,
412 oldProps,
413 newProps,
414 + oldState,
415 newState,
416 newContext
377 - )) {
378 - // TODO: Should this get the new props/state updated regardless?
379 - return false;
380 - }
417 + );
418
382 - if (typeof instance.componentWillUpdate === 'function') {
383 - instance.componentWillUpdate(newProps, newState, newContext);
419 + if (shouldUpdate) {
420 + markUpdate(workInProgress);
421 + if (typeof instance.componentWillUpdate === 'function') {
422 + instance.componentWillUpdate(newProps, newState, newContext);
423 + }
424 + } else {
425 + markUpdateIfAlreadyInProgress(current, workInProgress);
426 +
427 + // If shouldComponentUpdate returned false, we should still update the
428 + // memoized props/state to indicate that this work can be reused.
429 + memoizeProps(workInProgress, newProps);
430 + memoizeState(workInProgress, newState);
431 }
432
433 + // Update the existing instance's state, props, and context pointers even
434 + // if shouldComponentUpdate returns false.
435 instance.props = newProps;
436 instance.state = newState;
437 instance.context = newContext;
389 - return true;
438 +
439 + return shouldUpdate;
440 }
441
442 return {
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+3 -28
@@ -100,7 +100,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
100 }
101
102 function moveCoroutineToHandlerPhase(current : ?Fiber, workInProgress : Fiber) {
103 - var coroutine = (workInProgress.pendingProps : ?ReactCoroutine);
103 + var coroutine = (workInProgress.memoizedProps : ?ReactCoroutine);
104 if (!coroutine) {
105 throw new Error('Should be resolved by now');
106 }
@@ -170,24 +170,14 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
170
171 switch (workInProgress.tag) {
172 case FunctionalComponent:
173 - workInProgress.memoizedProps = workInProgress.pendingProps;
173 return null;
174 case ClassComponent: {
175 // We are leaving this subtree, so pop context if any.
176 popContextProvider(workInProgress);
178 - // Don't use the state queue to compute the memoized state. We already
179 - // merged it and assigned it to the instance. Transfer it from there.
180 - // Also need to transfer the props, because pendingProps will be null
181 - // in the case of an update.
182 - const instance = workInProgress.stateNode;
183 - workInProgress.memoizedState = instance.state;
184 - workInProgress.memoizedProps = instance.props;
185 -
177 return null;
178 }
179 case HostRoot: {
180 // TODO: Pop the host container after #8607 lands.
190 - workInProgress.memoizedProps = workInProgress.pendingProps;
181 const fiberRoot = (workInProgress.stateNode : FiberRoot);
182 if (fiberRoot.pendingContext) {
183 fiberRoot.context = fiberRoot.pendingContext;
@@ -198,7 +188,7 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
188 case HostComponent:
189 popHostContext(workInProgress);
190 const type = workInProgress.type;
201 - let newProps = workInProgress.pendingProps;
191 + const newProps = workInProgress.memoizedProps;
192 if (current && workInProgress.stateNode != null) {
193 // If we have an alternate, that means this is an update and we need to
194 // schedule a side-effect to do the updates.
@@ -207,9 +197,6 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
197 // have newProps so we'll have to reuse them.
198 // TODO: Split the update API as separate for the props vs. children.
199 // Even better would be if children weren't special cased at all tho.
210 - if (!newProps) {
211 - newProps = workInProgress.memoizedProps || oldProps;
212 - }
200 const instance : I = workInProgress.stateNode;
201 const currentHostContext = getHostContext();
202 if (prepareUpdate(instance, type, oldProps, newProps, currentHostContext)) {
@@ -255,20 +242,11 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
242 markUpdate(workInProgress);
243 }
244 }
258 - workInProgress.memoizedProps = newProps;
245 return null;
246 case HostText:
261 - let newText = workInProgress.pendingProps;
247 + let newText = workInProgress.memoizedProps;
248 if (current && workInProgress.stateNode != null) {
249 const oldText = current.memoizedProps;
264 - if (newText === null) {
265 - // If this was a bail out we need to fall back to memoized text.
266 - // This works the same way as HostComponent.
267 - newText = workInProgress.memoizedProps;
268 - if (newText === null) {
269 - newText = oldText;
270 - }
271 - }
250 // If we have an alternate, that means this is an update and we need
251 // to schedule a side-effect to do the updates.
252 if (oldText !== newText) {
@@ -293,7 +271,6 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
271 case CoroutineComponent:
272 return moveCoroutineToHandlerPhase(current, workInProgress);
273 case CoroutineHandlerPhase:
296 - workInProgress.memoizedProps = workInProgress.pendingProps;
274 // Reset the tag to now be a first phase coroutine.
275 workInProgress.tag = CoroutineComponent;
276 return null;
@@ -301,12 +278,10 @@ module.exports = function<T, P, I, TI, PI, C, CX>(
278 // Does nothing.
279 return null;
280 case Fragment:
304 - workInProgress.memoizedProps = workInProgress.pendingProps;
281 return null;
282 case HostPortal:
283 // TODO: Only mark this as an update if we have any pending callbacks.
284 markUpdate(workInProgress);
309 - workInProgress.memoizedProps = workInProgress.pendingProps;
285 popHostContainer(workInProgress);
286 return null;
287
src/renderers/shared/fiber/ReactFiberScheduler.js
-4
@@ -442,10 +442,6 @@ module.exports = function<T, P, I, TI, PI, C, CX>(config : HostConfig<T, P, I, T
442 const current = workInProgress.alternate;
443 const next = completeWork(current, workInProgress);
444
445 - // The work is now done. We don't need this anymore. This flags
446 - // to the system not to redo any work here.
447 - workInProgress.pendingProps = null;
448 -
445 const returnFiber = workInProgress.return;
446 const siblingFiber = workInProgress.sibling;
447
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+43 -6
@@ -628,6 +628,43 @@ describe('ReactIncremental', () => {
628
629 });
630
631 + it('memoizes work even if shouldComponentUpdate returns false', () => {
632 + let ops = [];
633 + class Foo extends React.Component {
634 + shouldComponentUpdate(nextProps) {
635 + // this.props is the memoized props. So this should return true for
636 + // every update except the first one.
637 + const shouldUpdate = this.props.step !== 1;
638 + ops.push('shouldComponentUpdate: ' + shouldUpdate);
639 + return shouldUpdate;
640 + }
641 + render() {
642 + ops.push('render');
643 + return <div />;
644 + }
645 + }
646 +
647 + ReactNoop.render(<Foo step={1} />);
648 + ReactNoop.flush();
649 +
650 + ops = [];
651 + ReactNoop.render(<Foo step={2} />);
652 + ReactNoop.flush();
653 + expect(ops).toEqual([
654 + 'shouldComponentUpdate: false',
655 + ]);
656 +
657 + ops = [];
658 + ReactNoop.render(<Foo step={3} />);
659 + ReactNoop.flush();
660 + expect(ops).toEqual([
661 + // If the memoized props were not updated during last bail out, sCU will
662 + // keep returning false.
663 + 'shouldComponentUpdate: true',
664 + 'render',
665 + ]);
666 + });
667 +
668 it('can update in the middle of a tree using setState', () => {
669 let instance;
670 class Bar extends React.Component {
@@ -873,13 +910,13 @@ describe('ReactIncremental', () => {
910
911 ReactNoop.render(<Foo step={1} />);
912 ReactNoop.flushDeferredPri(50);
876 - // A completed and was reused. B completed but couldn't be reused because
877 - // props differences. C didn't complete and therefore couldn't be reused.
878 - // D never even started so it needed a new instance.
879 - expect(ops).toEqual(['Foo', 'Bar:B2', 'Bar:C', 'Bar:D']);
913 + // A was memoized and reused. B was memoized but couldn't be reused because
914 + // props differences. C was memoized and reused. D never even started so it
915 + // needed a new instance.
916 + expect(ops).toEqual(['Foo', 'Bar:B2', 'Bar:D']);
917
918 // We expect each rerender to correspond to a new instance.
882 - expect(instances.size).toBe(6);
919 + expect(instances.size).toBe(5);
920 });
921
922 it('gets new props when setting state on a partly updated component', () => {
@@ -935,7 +972,7 @@ describe('ReactIncremental', () => {
972 ops = [];
973
974 ReactNoop.flush();
938 - expect(ops).toEqual(['Bar:A-1', 'Baz', 'Baz']);
975 + expect(ops).toEqual(['Bar:A-1', 'Baz']);
976 });
977
978 it('calls componentWillMount twice if the initial render is aborted', () => {
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+2 -2
@@ -667,7 +667,7 @@ describe('ReactIncrementalSideEffects', () => {
667 ),
668 ]);
669
670 - expect(ops).toEqual(['Foo', 'Baz', 'Bar']);
670 + expect(ops).toEqual(['Foo']);
671 ops = [];
672
673 ReactNoop.flush();
@@ -861,7 +861,7 @@ describe('ReactIncrementalSideEffects', () => {
861 ),
862 ]);
863
864 - expect(ops).toEqual(['Bar', 'Bar']);
864 + expect(ops).toEqual(['Bar']);
865 });
866 // TODO: Test that side-effects are not cut off when a work in progress node
867 // moves to "current" without flushing due to having lower priority. Does this