Fixes bug when initial mount of a host component is `hidden` (#12294)
`oldProps` was null. This went uncaught by the unit tests because ReactNoop did not use `oldProps` in either `prepareUpdate` or `completeUpdate`. I added some invariants so we don't regress in the future.
Andrew Clark committed
Feb 26, 2018 at 17:50 UTC
8e5f12ca6c18fb48ea71cba6bfa9bd7cf499ccb6
3 files changed
+24
-3
packages/react-noop-renderer/src/ReactNoop.js
+9
@@ -130,6 +130,12 @@ let SharedHostConfig = {
130
oldProps: Props,
131
newProps: Props,
132
): null | {} {
133
+ if (oldProps === null) {
134
+ throw new Error('Should have old props');
135
+ }
136
+ if (newProps === null) {
137
+ throw new Error('Should have old props');
138
+ }
139
return UPDATE_SIGNAL;
140
},
141
@@ -196,6 +202,9 @@ const NoopRenderer = ReactFiberReconciler({
202
oldProps: Props,
203
newProps: Props,
204
): void {
205
+ if (oldProps === null) {
206
+ throw new Error('Should have old props');
207
+ }
208
instance.prop = newProps.prop;
209
},
210
packages/react-reconciler/src/ReactFiberBeginWork.js
+13
-1
@@ -474,7 +474,18 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
474
// Normally we can bail out on props equality but if context has changed
475
// we don't do the bailout and we have to reuse existing props instead.
476
} else if (memoizedProps === nextProps) {
477
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
477
+ const isHidden =
478
+ workInProgress.mode & AsyncMode &&
479
+ shouldDeprioritizeSubtree(type, nextProps);
480
+ if (isHidden) {
481
+ // Before bailing out, make sure we've deprioritized a hidden component.
482
+ workInProgress.expirationTime = Never;
483
+ }
484
+ if (!isHidden || renderExpirationTime !== Never) {
485
+ return bailoutOnAlreadyFinishedWork(current, workInProgress);
486
+ }
487
+ // If we're rendering a hidden node at hidden priority, don't bailout. The
488
+ // parent is complete, but the children may not be.
489
}
490
491
let nextChildren = nextProps.children;
@@ -503,6 +514,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
514
// Down-prioritize the children.
515
workInProgress.expirationTime = Never;
516
// Bailout and come back to this fiber later.
517
+ workInProgress.memoizedProps = nextProps;
518
return null;
519
}
520
packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js
+2
-2
@@ -758,7 +758,7 @@ describe('ReactIncrementalSideEffects', () => {
758
// items. Including the set state since that is deprioritized.
759
// TODO: The cycles it takes to do this could be lowered with further
760
// optimizations.
761
- ReactNoop.flushDeferredPri(60 + 5);
761
+ ReactNoop.flushDeferredPri(35);
762
expect(ReactNoop.getChildren()).toEqual([
763
div(
764
// Updated.
@@ -790,7 +790,7 @@ describe('ReactIncrementalSideEffects', () => {
790
),
791
]);
792
793
- expect(ops).toEqual(['Bar', 'Bar']);
793
+ expect(ops).toEqual(['Bar', 'Bar', 'Bar']);
794
});
795
// TODO: Test that side-effects are not cut off when a work in progress node
796
// moves to "current" without flushing due to having lower priority. Does this