@samitouri / QOS-React / commits / 2b509e2c8c

[Experimental] API for reading context from within any render phase function (#13139)

* Store list of contexts on the fiber Currently, context can only be read by a special type of component, ContextConsumer. We want to add support to all fibers, including classes and functional components. Each fiber may read from one or more contexts. To enable quick, mono- morphic access of this list, we'll store them on a fiber property. * Context.unstable_read unstable_read can be called anywhere within the render phase. That includes the render method, getDerivedStateFromProps, constructors, functional components, and context consumer render props. If it's called outside the render phase, an error is thrown. * Remove vestigial context cursor Wasn't being used. * Split fiber.expirationTime into two separate fields Currently, the `expirationTime` field represents the pending work of both the fiber itself — including new props, state, and context — and of any updates in that fiber's subtree. This commit adds a second field called `childExpirationTime`. Now `expirationTime` only represents the pending work of the fiber itself. The subtree's pending work is represented by `childExpirationTime`. The biggest advantage is it requires fewer checks to bailout on already finished work. For most types of work, if the `expirationTime` does not match the render expiration time, we can bailout immediately without any further checks. This won't work for fibers that have `shouldComponentUpdate` semantics (class components), for which we still need to check for props and state changes explicitly. * Performance nits Optimize `readContext` for most common case

Andrew Clark committed Jul 20, 2018 at 16:49 UTC 2b509e2c8c8c1fbc33ae59ce7c7dca0107732ff4
14 files changed +1842 -1468
packages/react-noop-renderer/src/createReactNoop.js
+4 -1
@@ -662,7 +662,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
662 '- ' +
663 // need to explicitly coerce Symbol to a string
664 (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'),
665 - '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
665 + '[' +
666 + fiber.childExpirationTime +
667 + (fiber.pendingProps ? '*' : '') +
668 + ']',
669 );
670 if (fiber.updateQueue) {
671 logUpdateQueue(fiber.updateQueue, depth);
packages/react-reconciler/src/ReactFiber.js
+22 -2
@@ -14,6 +14,7 @@ import type {TypeOfMode} from './ReactTypeOfMode';
14 import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect';
15 import type {ExpirationTime} from './ReactFiberExpirationTime';
16 import type {UpdateQueue} from './ReactUpdateQueue';
17 +import type {ContextDependency} from './ReactFiberNewContext';
18
19 import invariant from 'shared/invariant';
20 import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
@@ -124,6 +125,9 @@ export type Fiber = {|
125 // The state used to create the output
126 memoizedState: any,
127
128 + // A linked-list of contexts that this fiber depends on
129 + firstContextDependency: ContextDependency<mixed> | null,
130 +
131 // Bitfield that describes properties about the fiber and its subtree. E.g.
132 // the AsyncMode flag indicates whether the subtree should be async-by-
133 // default. When a fiber is created, it inherits the mode of its
@@ -145,9 +149,12 @@ export type Fiber = {|
149 lastEffect: Fiber | null,
150
151 // Represents a time in the future by which this work should be completed.
148 - // This is also used to quickly determine if a subtree has no pending changes.
152 + // Does not include work found in its subtree.
153 expirationTime: ExpirationTime,
154
155 + // This is used to quickly determine if a subtree has no pending changes.
156 + childExpirationTime: ExpirationTime,
157 +
158 // This is a pooled version of a Fiber. Every fiber that gets updated will
159 // eventually have a pair. There are cases when we can clean up pairs to save
160 // memory if we need to.
@@ -213,6 +220,7 @@ function FiberNode(
220 this.memoizedProps = null;
221 this.updateQueue = null;
222 this.memoizedState = null;
223 + this.firstContextDependency = null;
224
225 this.mode = mode;
226
@@ -224,6 +232,7 @@ function FiberNode(
232 this.lastEffect = null;
233
234 this.expirationTime = NoWork;
235 + this.childExpirationTime = NoWork;
236
237 this.alternate = null;
238
@@ -325,12 +334,21 @@ export function createWorkInProgress(
334 }
335 }
336
328 - workInProgress.expirationTime = expirationTime;
337 + // Don't touching the subtree's expiration time, which has not changed.
338 + workInProgress.childExpirationTime = current.childExpirationTime;
339 + if (pendingProps !== current.pendingProps) {
340 + // This fiber has new props.
341 + workInProgress.expirationTime = expirationTime;
342 + } else {
343 + // This fiber's props have not changed.
344 + workInProgress.expirationTime = current.expirationTime;
345 + }
346
347 workInProgress.child = current.child;
348 workInProgress.memoizedProps = current.memoizedProps;
349 workInProgress.memoizedState = current.memoizedState;
350 workInProgress.updateQueue = current.updateQueue;
351 + workInProgress.firstContextDependency = current.firstContextDependency;
352
353 // These will be overridden during the parent's reconciliation
354 workInProgress.sibling = current.sibling;
@@ -559,12 +577,14 @@ export function assignFiberPropertiesInDEV(
577 target.memoizedProps = source.memoizedProps;
578 target.updateQueue = source.updateQueue;
579 target.memoizedState = source.memoizedState;
580 + target.firstContextDependency = source.firstContextDependency;
581 target.mode = source.mode;
582 target.effectTag = source.effectTag;
583 target.nextEffect = source.nextEffect;
584 target.firstEffect = source.firstEffect;
585 target.lastEffect = source.lastEffect;
586 target.expirationTime = source.expirationTime;
587 + target.childExpirationTime = source.childExpirationTime;
588 target.alternate = source.alternate;
589 if (enableProfilerTimer) {
590 target.actualDuration = source.actualDuration;
packages/react-reconciler/src/ReactFiberBeginWork.js
+256 -373
@@ -70,8 +70,9 @@ import {
70 import {pushHostContext, pushHostContainer} from './ReactFiberHostContext';
71 import {
72 pushProvider,
73 - getContextCurrentValue,
74 - getContextChangedBits,
73 + propagateContextChange,
74 + readContext,
75 + prepareToReadContext,
76 } from './ReactFiberNewContext';
77 import {
78 markActualRenderTimeStarted,
@@ -111,17 +112,7 @@ if (__DEV__) {
112 didWarnAboutStatelessRefs = {};
113 }
114
114 -// TODO: Remove this and use reconcileChildrenAtExpirationTime directly.
115 -function reconcileChildren(current, workInProgress, nextChildren) {
116 - reconcileChildrenAtExpirationTime(
117 - current,
118 - workInProgress,
119 - nextChildren,
120 - workInProgress.expirationTime,
121 - );
122 -}
123 -
124 -export function reconcileChildrenAtExpirationTime(
115 +export function reconcileChildren(
116 current: Fiber | null,
117 workInProgress: Fiber,
118 nextChildren: any,
@@ -154,7 +145,7 @@ export function reconcileChildrenAtExpirationTime(
145 }
146 }
147
157 -function updateForwardRef(current, workInProgress) {
148 +function updateForwardRef(current, workInProgress, renderExpirationTime) {
149 const render = workInProgress.type.render;
150 const nextProps = workInProgress.pendingProps;
151 const ref = workInProgress.ref;
@@ -164,7 +155,11 @@ function updateForwardRef(current, workInProgress) {
155 } else if (workInProgress.memoizedProps === nextProps) {
156 const currentRef = current !== null ? current.ref : null;
157 if (ref === currentRef) {
167 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
158 + return bailoutOnAlreadyFinishedWork(
159 + current,
160 + workInProgress,
161 + renderExpirationTime,
162 + );
163 }
164 }
165
@@ -178,50 +173,52 @@ function updateForwardRef(current, workInProgress) {
173 nextChildren = render(nextProps, ref);
174 }
175
181 - reconcileChildren(current, workInProgress, nextChildren);
176 + reconcileChildren(
177 + current,
178 + workInProgress,
179 + nextChildren,
180 + renderExpirationTime,
181 + );
182 memoizeProps(workInProgress, nextProps);
183 return workInProgress.child;
184 }
185
186 -function updateFragment(current, workInProgress) {
186 +function updateFragment(current, workInProgress, renderExpirationTime) {
187 const nextChildren = workInProgress.pendingProps;
188 - if (hasLegacyContextChanged()) {
189 - // Normally we can bail out on props equality but if context has changed
190 - // we don't do the bailout and we have to reuse existing props instead.
191 - } else if (workInProgress.memoizedProps === nextChildren) {
192 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
193 - }
194 - reconcileChildren(current, workInProgress, nextChildren);
188 + reconcileChildren(
189 + current,
190 + workInProgress,
191 + nextChildren,
192 + renderExpirationTime,
193 + );
194 memoizeProps(workInProgress, nextChildren);
195 return workInProgress.child;
196 }
197
199 -function updateMode(current, workInProgress) {
198 +function updateMode(current, workInProgress, renderExpirationTime) {
199 const nextChildren = workInProgress.pendingProps.children;
201 - if (hasLegacyContextChanged()) {
202 - // Normally we can bail out on props equality but if context has changed
203 - // we don't do the bailout and we have to reuse existing props instead.
204 - } else if (
205 - nextChildren === null ||
206 - workInProgress.memoizedProps === nextChildren
207 - ) {
208 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
209 - }
210 - reconcileChildren(current, workInProgress, nextChildren);
200 + reconcileChildren(
201 + current,
202 + workInProgress,
203 + nextChildren,
204 + renderExpirationTime,
205 + );
206 memoizeProps(workInProgress, nextChildren);
207 return workInProgress.child;
208 }
209
215 -function updateProfiler(current, workInProgress) {
216 - const nextProps = workInProgress.pendingProps;
210 +function updateProfiler(current, workInProgress, renderExpirationTime) {
211 if (enableProfilerTimer) {
212 workInProgress.effectTag |= Update;
213 }
220 - if (workInProgress.memoizedProps === nextProps) {
221 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
222 - }
214 + const nextProps = workInProgress.pendingProps;
215 const nextChildren = nextProps.children;
224 - reconcileChildren(current, workInProgress, nextChildren);
216 + reconcileChildren(
217 + current,
218 + workInProgress,
219 + nextChildren,
220 + renderExpirationTime,
221 + );
222 memoizeProps(workInProgress, nextProps);
223 return workInProgress.child;
224 }
@@ -237,26 +234,18 @@ function markRef(current: Fiber | null, workInProgress: Fiber) {
234 }
235 }
236
240 -function updateFunctionalComponent(current, workInProgress) {
237 +function updateFunctionalComponent(
238 + current,
239 + workInProgress,
240 + renderExpirationTime,
241 +) {
242 const fn = workInProgress.type;
243 const nextProps = workInProgress.pendingProps;
243 -
244 - if (hasLegacyContextChanged()) {
245 - // Normally we can bail out on props equality but if context has changed
246 - // we don't do the bailout and we have to reuse existing props instead.
247 - } else {
248 - if (workInProgress.memoizedProps === nextProps) {
249 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
250 - }
251 - // TODO: consider bringing fn.shouldComponentUpdate() back.
252 - // It used to be here.
253 - }
254 -
244 const unmaskedContext = getUnmaskedContext(workInProgress);
245 const context = getMaskedContext(workInProgress, unmaskedContext);
246
247 let nextChildren;
259 -
248 + prepareToReadContext(workInProgress, renderExpirationTime);
249 if (__DEV__) {
250 ReactCurrentOwner.current = workInProgress;
251 ReactCurrentFiber.setCurrentPhase('render');
@@ -265,9 +254,15 @@ function updateFunctionalComponent(current, workInProgress) {
254 } else {
255 nextChildren = fn(nextProps, context);
256 }
257 +
258 // React DevTools reads this flag.
259 workInProgress.effectTag |= PerformedWork;
270 - reconcileChildren(current, workInProgress, nextChildren);
260 + reconcileChildren(
261 + current,
262 + workInProgress,
263 + nextChildren,
264 + renderExpirationTime,
265 + );
266 memoizeProps(workInProgress, nextProps);
267 return workInProgress.child;
268 }
@@ -281,6 +276,11 @@ function updateClassComponent(
276 // During mounting we don't know the child context yet as the instance doesn't exist.
277 // We will invalidate the child context in finishClassComponent() right after rendering.
278 const hasContext = pushLegacyContextProvider(workInProgress);
279 + const hasPendingNewContext = prepareToReadContext(
280 + workInProgress,
281 + renderExpirationTime,
282 + );
283 +
284 let shouldUpdate;
285 if (current === null) {
286 if (workInProgress.stateNode === null) {
@@ -297,6 +297,7 @@ function updateClassComponent(
297 // In a resume, we'll already have an instance we can reuse.
298 shouldUpdate = resumeMountClassInstance(
299 workInProgress,
300 + hasPendingNewContext,
301 renderExpirationTime,
302 );
303 }
@@ -304,6 +305,7 @@ function updateClassComponent(
305 shouldUpdate = updateClassInstance(
306 current,
307 workInProgress,
308 + hasPendingNewContext,
309 renderExpirationTime,
310 );
311 }
@@ -334,7 +336,11 @@ function finishClassComponent(
336 invalidateContextProvider(workInProgress, false);
337 }
338
337 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
339 + return bailoutOnAlreadyFinishedWork(
340 + current,
341 + workInProgress,
342 + renderExpirationTime,
343 + );
344 }
345
346 const ctor = workInProgress.type;
@@ -380,18 +386,13 @@ function finishClassComponent(
386 if (current !== null && didCaptureError) {
387 // If we're recovering from an error, reconcile twice: first to delete
388 // all the existing children.
383 - reconcileChildrenAtExpirationTime(
384 - current,
385 - workInProgress,
386 - null,
387 - renderExpirationTime,
388 - );
389 + reconcileChildren(current, workInProgress, null, renderExpirationTime);
390 workInProgress.child = null;
391 // Now we can continue reconciling like normal. This has the effect of
392 // remounting all children regardless of whether their their
393 // identity matches.
394 }
394 - reconcileChildrenAtExpirationTime(
395 + reconcileChildren(
396 current,
397 workInProgress,
398 nextChildren,
@@ -427,66 +428,75 @@ function pushHostRootContext(workInProgress) {
428
429 function updateHostRoot(current, workInProgress, renderExpirationTime) {
430 pushHostRootContext(workInProgress);
430 - let updateQueue = workInProgress.updateQueue;
431 - if (updateQueue !== null) {
432 - const nextProps = workInProgress.pendingProps;
433 - const prevState = workInProgress.memoizedState;
434 - const prevChildren = prevState !== null ? prevState.element : null;
435 - processUpdateQueue(
431 + const updateQueue = workInProgress.updateQueue;
432 + invariant(
433 + updateQueue !== null,
434 + 'If the root does not have an updateQueue, we should have already ' +
435 + 'bailed out. This error is likely caused by a bug in React. Please ' +
436 + 'file an issue.',
437 + );
438 + const nextProps = workInProgress.pendingProps;
439 + const prevState = workInProgress.memoizedState;
440 + const prevChildren = prevState !== null ? prevState.element : null;
441 + processUpdateQueue(
442 + workInProgress,
443 + updateQueue,
444 + nextProps,
445 + null,
446 + renderExpirationTime,
447 + );
448 + const nextState = workInProgress.memoizedState;
449 + // Caution: React DevTools currently depends on this property
450 + // being called "element".
451 + const nextChildren = nextState.element;
452 + if (nextChildren === prevChildren) {
453 + // If the state is the same as before, that's a bailout because we had
454 + // no work that expires at this time.
455 + resetHydrationState();
456 + return bailoutOnAlreadyFinishedWork(
457 + current,
458 + workInProgress,
459 + renderExpirationTime,
460 + );
461 + }
462 + const root: FiberRoot = workInProgress.stateNode;
463 + if (
464 + (current === null || current.child === null) &&
465 + root.hydrate &&
466 + enterHydrationState(workInProgress)
467 + ) {
468 + // If we don't have any current children this might be the first pass.
469 + // We always try to hydrate. If this isn't a hydration pass there won't
470 + // be any children to hydrate which is effectively the same thing as
471 + // not hydrating.
472 +
473 + // This is a bit of a hack. We track the host root as a placement to
474 + // know that we're currently in a mounting state. That way isMounted
475 + // works as expected. We must reset this before committing.
476 + // TODO: Delete this when we delete isMounted and findDOMNode.
477 + workInProgress.effectTag |= Placement;
478 +
479 + // Ensure that children mount into this root without tracking
480 + // side-effects. This ensures that we don't store Placement effects on
481 + // nodes that will be hydrated.
482 + workInProgress.child = mountChildFibers(
483 workInProgress,
437 - updateQueue,
438 - nextProps,
484 null,
485 + nextChildren,
486 renderExpirationTime,
487 );
442 - const nextState = workInProgress.memoizedState;
443 - // Caution: React DevTools currently depends on this property
444 - // being called "element".
445 - const nextChildren = nextState.element;
446 -
447 - if (nextChildren === prevChildren) {
448 - // If the state is the same as before, that's a bailout because we had
449 - // no work that expires at this time.
450 - resetHydrationState();
451 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
452 - }
453 - const root: FiberRoot = workInProgress.stateNode;
454 - if (
455 - (current === null || current.child === null) &&
456 - root.hydrate &&
457 - enterHydrationState(workInProgress)
458 - ) {
459 - // If we don't have any current children this might be the first pass.
460 - // We always try to hydrate. If this isn't a hydration pass there won't
461 - // be any children to hydrate which is effectively the same thing as
462 - // not hydrating.
463 -
464 - // This is a bit of a hack. We track the host root as a placement to
465 - // know that we're currently in a mounting state. That way isMounted
466 - // works as expected. We must reset this before committing.
467 - // TODO: Delete this when we delete isMounted and findDOMNode.
468 - workInProgress.effectTag |= Placement;
469 -
470 - // Ensure that children mount into this root without tracking
471 - // side-effects. This ensures that we don't store Placement effects on
472 - // nodes that will be hydrated.
473 - workInProgress.child = mountChildFibers(
474 - workInProgress,
475 - null,
476 - nextChildren,
477 - renderExpirationTime,
478 - );
479 - } else {
480 - // Otherwise reset hydration state in case we aborted and resumed another
481 - // root.
482 - resetHydrationState();
483 - reconcileChildren(current, workInProgress, nextChildren);
484 - }
485 - return workInProgress.child;
488 + } else {
489 + // Otherwise reset hydration state in case we aborted and resumed another
490 + // root.
491 + reconcileChildren(
492 + current,
493 + workInProgress,
494 + nextChildren,
495 + renderExpirationTime,
496 + );
497 + resetHydrationState();
498 }
487 - resetHydrationState();
488 - // If there is no update queue, that's a bailout because the root has no props.
489 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
499 + return workInProgress.child;
500 }
501
502 function updateHostComponent(current, workInProgress, renderExpirationTime) {
@@ -497,28 +507,9 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
507 }
508
509 const type = workInProgress.type;
500 - const memoizedProps = workInProgress.memoizedProps;
510 const nextProps = workInProgress.pendingProps;
511 const prevProps = current !== null ? current.memoizedProps : null;
512
504 - if (hasLegacyContextChanged()) {
505 - // Normally we can bail out on props equality but if context has changed
506 - // we don't do the bailout and we have to reuse existing props instead.
507 - } else if (memoizedProps === nextProps) {
508 - const isHidden =
509 - workInProgress.mode & AsyncMode &&
510 - shouldDeprioritizeSubtree(type, nextProps);
511 - if (isHidden) {
512 - // Before bailing out, make sure we've deprioritized a hidden component.
513 - workInProgress.expirationTime = Never;
514 - }
515 - if (!isHidden || renderExpirationTime !== Never) {
516 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
517 - }
518 - // If we're rendering a hidden node at hidden priority, don't bailout. The
519 - // parent is complete, but the children may not be.
520 - }
521 -
513 let nextChildren = nextProps.children;
514 const isDirectTextChild = shouldSetTextContent(type, nextProps);
515
@@ -528,7 +519,7 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
519 // this in the host environment that also have access to this prop. That
520 // avoids allocating another HostText fiber and traversing it.
521 nextChildren = null;
531 - } else if (prevProps && shouldSetTextContent(type, prevProps)) {
522 + } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
523 // If we're switching from a direct text child to a normal child, or to
524 // empty, we need to schedule the text content to be reset.
525 workInProgress.effectTag |= ContentReset;
@@ -542,14 +533,18 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
533 workInProgress.mode & AsyncMode &&
534 shouldDeprioritizeSubtree(type, nextProps)
535 ) {
545 - // Down-prioritize the children.
536 + // Schedule this fiber to re-render at offscreen priority. Then bailout.
537 workInProgress.expirationTime = Never;
547 - // Bailout and come back to this fiber later.
538 workInProgress.memoizedProps = nextProps;
539 return null;
540 }
541
552 - reconcileChildren(current, workInProgress, nextChildren);
542 + reconcileChildren(
543 + current,
544 + workInProgress,
545 + nextChildren,
546 + renderExpirationTime,
547 + );
548 memoizeProps(workInProgress, nextProps);
549 return workInProgress.child;
550 }
@@ -580,6 +575,8 @@ function mountIndeterminateComponent(
575 const unmaskedContext = getUnmaskedContext(workInProgress);
576 const context = getMaskedContext(workInProgress, unmaskedContext);
577
578 + prepareToReadContext(workInProgress, renderExpirationTime);
579 +
580 let value;
581
582 if (__DEV__) {
@@ -697,7 +694,7 @@ function mountIndeterminateComponent(
694 }
695 }
696 }
700 - reconcileChildren(current, workInProgress, value);
697 + reconcileChildren(current, workInProgress, value, renderExpirationTime);
698 memoizeProps(workInProgress, props);
699 return workInProgress.child;
700 }
@@ -710,9 +707,6 @@ function updatePlaceholderComponent(
707 ) {
708 if (enableSuspense) {
709 const nextProps = workInProgress.pendingProps;
713 - const prevProps = workInProgress.memoizedProps;
714 -
715 - const prevDidTimeout = workInProgress.memoizedState === true;
710
711 // Check if we already attempted to render the normal state. If we did,
712 // and we timed out, render the placeholder state.
@@ -727,12 +721,7 @@ function updatePlaceholderComponent(
721 nextDidTimeout = true;
722 // If we're recovering from an error, reconcile twice: first to delete
723 // all the existing children.
730 - reconcileChildrenAtExpirationTime(
731 - current,
732 - workInProgress,
733 - null,
734 - renderExpirationTime,
735 - );
724 + reconcileChildren(current, workInProgress, null, renderExpirationTime);
725 current.child = null;
726 // Now we can continue reconciling like normal. This has the effect of
727 // remounting all children regardless of whether their their
@@ -741,13 +730,6 @@ function updatePlaceholderComponent(
730 nextDidTimeout = !alreadyCaptured;
731 }
732
744 - if (hasLegacyContextChanged()) {
745 - // Normally we can bail out on props equality but if context has changed
746 - // we don't do the bailout and we have to reuse existing props instead.
747 - } else if (nextProps === prevProps && nextDidTimeout === prevDidTimeout) {
748 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
749 - }
750 -
733 if ((workInProgress.mode & StrictMode) !== NoEffect) {
734 if (nextDidTimeout) {
735 // If the timed-out view commits, schedule an update effect to record
@@ -772,7 +754,12 @@ function updatePlaceholderComponent(
754
755 workInProgress.memoizedProps = nextProps;
756 workInProgress.memoizedState = nextDidTimeout;
775 - reconcileChildren(current, workInProgress, nextChildren);
757 + reconcileChildren(
758 + current,
759 + workInProgress,
760 + nextChildren,
761 + renderExpirationTime,
762 + );
763 return workInProgress.child;
764 } else {
765 return null;
@@ -782,13 +769,6 @@ function updatePlaceholderComponent(
769 function updatePortalComponent(current, workInProgress, renderExpirationTime) {
770 pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
771 const nextChildren = workInProgress.pendingProps;
785 - if (hasLegacyContextChanged()) {
786 - // Normally we can bail out on props equality but if context has changed
787 - // we don't do the bailout and we have to reuse existing props instead.
788 - } else if (workInProgress.memoizedProps === nextChildren) {
789 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
790 - }
791 -
772 if (current === null) {
773 // Portals are special because we don't append the children during mount
774 // but at commit. Therefore we need to track insertions which the normal
@@ -803,125 +783,23 @@ function updatePortalComponent(current, workInProgress, renderExpirationTime) {
783 );
784 memoizeProps(workInProgress, nextChildren);
785 } else {
806 - reconcileChildren(current, workInProgress, nextChildren);
786 + reconcileChildren(
787 + current,
788 + workInProgress,
789 + nextChildren,
790 + renderExpirationTime,
791 + );
792 memoizeProps(workInProgress, nextChildren);
793 }
794 return workInProgress.child;
795 }
796
812 -function propagateContextChange<V>(
813 - workInProgress: Fiber,
814 - context: ReactContext<V>,
815 - changedBits: number,
816 - renderExpirationTime: ExpirationTime,
817 -): void {
818 - let fiber = workInProgress.child;
819 - if (fiber !== null) {
820 - // Set the return pointer of the child to the work-in-progress fiber.
821 - fiber.return = workInProgress;
822 - }
823 - while (fiber !== null) {
824 - let nextFiber;
825 - // Visit this fiber.
826 - switch (fiber.tag) {
827 - case ContextConsumer:
828 - // Check if the context matches.
829 - const observedBits: number = fiber.stateNode | 0;
830 - if (fiber.type === context && (observedBits & changedBits) !== 0) {
831 - // Update the expiration time of all the ancestors, including
832 - // the alternates.
833 -
834 - let node = fiber;
835 - do {
836 - const alternate = node.alternate;
837 - if (
838 - node.expirationTime === NoWork ||
839 - node.expirationTime > renderExpirationTime
840 - ) {
841 - node.expirationTime = renderExpirationTime;
842 - if (
843 - alternate !== null &&
844 - (alternate.expirationTime === NoWork ||
845 - alternate.expirationTime > renderExpirationTime)
846 - ) {
847 - alternate.expirationTime = renderExpirationTime;
848 - }
849 - } else if (
850 - alternate !== null &&
851 - (alternate.expirationTime === NoWork ||
852 - alternate.expirationTime > renderExpirationTime)
853 - ) {
854 - alternate.expirationTime = renderExpirationTime;
855 - } else {
856 - // Neither alternate was updated, which means the rest of the
857 - // ancestor path already has sufficient priority.
858 - break;
859 - }
860 - node = node.return;
861 - } while (node !== null);
862 -
863 - // Don't scan deeper than a matching consumer. When we render the
864 - // consumer, we'll continue scanning from that point. This way the
865 - // scanning work is time-sliced.
866 - nextFiber = null;
867 - } else {
868 - // Traverse down.
869 - nextFiber = fiber.child;
870 - }
871 - break;
872 - case ContextProvider:
873 - // Don't scan deeper if this is a matching provider
874 - nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
875 - break;
876 - default:
877 - // Traverse down.
878 - nextFiber = fiber.child;
879 - break;
880 - }
881 - if (nextFiber !== null) {
882 - // Set the return pointer of the child to the work-in-progress fiber.
883 - nextFiber.return = fiber;
884 - } else {
885 - // No child. Traverse to next sibling.
886 - nextFiber = fiber;
887 - while (nextFiber !== null) {
888 - if (nextFiber === workInProgress) {
889 - // We're back to the root of this subtree. Exit.
890 - nextFiber = null;
891 - break;
892 - }
893 - let sibling = nextFiber.sibling;
894 - if (sibling !== null) {
895 - // Set the return pointer of the sibling to the work-in-progress fiber.
896 - sibling.return = nextFiber.return;
897 - nextFiber = sibling;
898 - break;
899 - }
900 - // No more siblings. Traverse up.
901 - nextFiber = nextFiber.return;
902 - }
903 - }
904 - fiber = nextFiber;
905 - }
906 -}
907 -
797 function updateContextProvider(current, workInProgress, renderExpirationTime) {
798 const providerType: ReactProviderType<any> = workInProgress.type;
799 const context: ReactContext<any> = providerType._context;
800
801 const newProps = workInProgress.pendingProps;
802 const oldProps = workInProgress.memoizedProps;
914 - let canBailOnProps = true;
915 -
916 - if (hasLegacyContextChanged()) {
917 - canBailOnProps = false;
918 - // Normally we can bail out on props equality but if context has changed
919 - // we don't do the bailout and we have to reuse existing props instead.
920 - } else if (oldProps === newProps) {
921 - workInProgress.stateNode = 0;
922 - pushProvider(workInProgress);
923 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
924 - }
803
804 const newValue = newProps.value;
805 workInProgress.memoizedProps = newProps;
@@ -947,10 +825,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) {
825 } else {
826 if (oldProps.value === newProps.value) {
827 // No change. Bailout early if children are the same.
950 - if (oldProps.children === newProps.children && canBailOnProps) {
828 + if (
829 + oldProps.children === newProps.children &&
830 + !hasLegacyContextChanged()
831 + ) {
832 workInProgress.stateNode = 0;
833 pushProvider(workInProgress);
953 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
834 + return bailoutOnAlreadyFinishedWork(
835 + current,
836 + workInProgress,
837 + renderExpirationTime,
838 + );
839 }
840 changedBits = 0;
841 } else {
@@ -964,10 +849,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) {
849 (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare
850 ) {
851 // No change. Bailout early if children are the same.
967 - if (oldProps.children === newProps.children && canBailOnProps) {
852 + if (
853 + oldProps.children === newProps.children &&
854 + !hasLegacyContextChanged()
855 + ) {
856 workInProgress.stateNode = 0;
857 pushProvider(workInProgress);
970 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
858 + return bailoutOnAlreadyFinishedWork(
859 + current,
860 + workInProgress,
861 + renderExpirationTime,
862 + );
863 }
864 changedBits = 0;
865 } else {
@@ -987,10 +879,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) {
879
880 if (changedBits === 0) {
881 // No change. Bailout early if children are the same.
990 - if (oldProps.children === newProps.children && canBailOnProps) {
882 + if (
883 + oldProps.children === newProps.children &&
884 + !hasLegacyContextChanged()
885 + ) {
886 workInProgress.stateNode = 0;
887 pushProvider(workInProgress);
993 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
888 + return bailoutOnAlreadyFinishedWork(
889 + current,
890 + workInProgress,
891 + renderExpirationTime,
892 + );
893 }
894 } else {
895 propagateContextChange(
@@ -1008,52 +907,13 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) {
907 pushProvider(workInProgress);
908
909 const newChildren = newProps.children;
1011 - reconcileChildren(current, workInProgress, newChildren);
910 + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
911 return workInProgress.child;
912 }
913
914 function updateContextConsumer(current, workInProgress, renderExpirationTime) {
915 const context: ReactContext<any> = workInProgress.type;
916 const newProps = workInProgress.pendingProps;
1018 - const oldProps = workInProgress.memoizedProps;
1019 -
1020 - const newValue = getContextCurrentValue(context);
1021 - const changedBits = getContextChangedBits(context);
1022 -
1023 - if (hasLegacyContextChanged()) {
1024 - // Normally we can bail out on props equality but if context has changed
1025 - // we don't do the bailout and we have to reuse existing props instead.
1026 - } else if (changedBits === 0 && oldProps === newProps) {
1027 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
1028 - }
1029 - workInProgress.memoizedProps = newProps;
1030 -
1031 - let observedBits = newProps.unstable_observedBits;
1032 - if (observedBits === undefined || observedBits === null) {
1033 - // Subscribe to all changes by default
1034 - observedBits = MAX_SIGNED_31_BIT_INT;
1035 - }
1036 - // Store the observedBits on the fiber's stateNode for quick access.
1037 - workInProgress.stateNode = observedBits;
1038 -
1039 - if ((changedBits & observedBits) !== 0) {
1040 - // Context change propagation stops at matching consumers, for time-
1041 - // slicing. Continue the propagation here.
1042 - propagateContextChange(
1043 - workInProgress,
1044 - context,
1045 - changedBits,
1046 - renderExpirationTime,
1047 - );
1048 - } else if (oldProps === newProps) {
1049 - // Skip over a memoized parent with a bitmask bailout even
1050 - // if we began working on it because of a deeper matching child.
1051 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
1052 - }
1053 - // There is no bailout on `children` equality because we expect people
1054 - // to often pass a bound method as a child, but it may reference
1055 - // `this.state` or `this.props` (and thus needs to re-render on `setState`).
1056 -
917 const render = newProps.children;
918
919 if (__DEV__) {
@@ -1066,6 +926,8 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) {
926 );
927 }
928
929 + prepareToReadContext(workInProgress, renderExpirationTime);
930 + const newValue = readContext(context, newProps.unstable_observedBits);
931 let newChildren;
932 if (__DEV__) {
933 ReactCurrentOwner.current = workInProgress;
@@ -1078,7 +940,8 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) {
940
941 // React DevTools reads this flag.
942 workInProgress.effectTag |= PerformedWork;
1081 - reconcileChildren(current, workInProgress, newChildren);
943 + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
944 + workInProgress.memoizedProps = newProps;
945 return workInProgress.child;
946 }
947
@@ -1102,61 +965,38 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) {
965 */
966
967 function bailoutOnAlreadyFinishedWork(
1105 - current,
968 + current: Fiber | null,
969 workInProgress: Fiber,
970 + renderExpirationTime: ExpirationTime,
971 ): Fiber | null {
972 cancelWorkTimer(workInProgress);
973
1110 - if (enableProfilerTimer) {
1111 - // Don't update "base" render times for bailouts.
1112 - stopBaseRenderTimerIfRunning();
974 + if (current !== null) {
975 + // Reuse previous context list
976 + workInProgress.firstContextDependency = current.firstContextDependency;
977 }
978
1115 - // TODO: We should ideally be able to bail out early if the children have no
1116 - // more work to do. However, since we don't have a separation of this
1117 - // Fiber's priority and its children yet - we don't know without doing lots
1118 - // of the same work we do anyway. Once we have that separation we can just
1119 - // bail out here if the children has no more work at this priority level.
1120 - // if (workInProgress.priorityOfChildren <= priorityLevel) {
1121 - // // If there are side-effects in these children that have not yet been
1122 - // // committed we need to ensure that they get properly transferred up.
1123 - // if (current && current.child !== workInProgress.child) {
1124 - // reuseChildrenEffects(workInProgress, child);
1125 - // }
1126 - // return null;
1127 - // }
1128 -
1129 - cloneChildFibers(current, workInProgress);
1130 - return workInProgress.child;
1131 -}
1132 -
1133 -function bailoutOnLowPriority(current, workInProgress) {
1134 - cancelWorkTimer(workInProgress);
1135 -
979 if (enableProfilerTimer) {
980 // Don't update "base" render times for bailouts.
981 stopBaseRenderTimerIfRunning();
982 }
983
1141 - // TODO: Handle HostComponent tags here as well and call pushHostContext()?
1142 - // See PR 8590 discussion for context
1143 - switch (workInProgress.tag) {
1144 - case HostRoot:
1145 - pushHostRootContext(workInProgress);
1146 - break;
1147 - case ClassComponent:
1148 - pushLegacyContextProvider(workInProgress);
1149 - break;
1150 - case HostPortal:
1151 - pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
1152 - break;
1153 - case ContextProvider:
1154 - pushProvider(workInProgress);
1155 - break;
984 + // Check if the children have any pending work.
985 + const childExpirationTime = workInProgress.childExpirationTime;
986 + if (
987 + childExpirationTime === NoWork ||
988 + childExpirationTime > renderExpirationTime
989 + ) {
990 + // The children don't have any work either. We can skip them.
991 + // TODO: Once we add back resuming, we should check if the children are
992 + // a work-in-progress set. If so, we need to transfer their effects.
993 + return null;
994 + } else {
995 + // This fiber doesn't have work, but its subtree does. Clone the child
996 + // fibers and continue.
997 + cloneChildFibers(current, workInProgress);
998 + return workInProgress.child;
999 }
1157 - // TODO: What if this is currently in progress?
1158 - // How can that happen? How is this not being cloned?
1159 - return null;
1000 }
1001
1002 // TODO: Delete memoizeProps/State and move to reconcile/bailout instead
@@ -1181,13 +1021,52 @@ function beginWork(
1021 }
1022 }
1023
1024 + const updateExpirationTime = workInProgress.expirationTime;
1025 if (
1185 - workInProgress.expirationTime === NoWork ||
1186 - workInProgress.expirationTime > renderExpirationTime
1026 + !hasLegacyContextChanged() &&
1027 + (updateExpirationTime === NoWork ||
1028 + updateExpirationTime > renderExpirationTime)
1029 ) {
1188 - return bailoutOnLowPriority(current, workInProgress);
1030 + // This fiber does not have any pending work. Bailout without entering
1031 + // the begin phase. There's still some bookkeeping we that needs to be done
1032 + // in this optimized path, mostly pushing stuff onto the stack.
1033 + switch (workInProgress.tag) {
1034 + case HostRoot:
1035 + pushHostRootContext(workInProgress);
1036 + resetHydrationState();
1037 + break;
1038 + case HostComponent:
1039 + pushHostContext(workInProgress);
1040 + break;
1041 + case ClassComponent:
1042 + pushLegacyContextProvider(workInProgress);
1043 + break;
1044 + case HostPortal:
1045 + pushHostContainer(
1046 + workInProgress,
1047 + workInProgress.stateNode.containerInfo,
1048 + );
1049 + break;
1050 + case ContextProvider:
1051 + workInProgress.stateNode = 0;
1052 + pushProvider(workInProgress);
1053 + break;
1054 + case Profiler:
1055 + if (enableProfilerTimer) {
1056 + workInProgress.effectTag |= Update;
1057 + }
1058 + break;
1059 + }
1060 + return bailoutOnAlreadyFinishedWork(
1061 + current,
1062 + workInProgress,
1063 + renderExpirationTime,
1064 + );
1065 }
1066
1067 + // Before entering the begin phase, clear the expiration time.
1068 + workInProgress.expirationTime = NoWork;
1069 +
1070 switch (workInProgress.tag) {
1071 case IndeterminateComponent:
1072 return mountIndeterminateComponent(
@@ -1196,7 +1075,11 @@ function beginWork(
1075 renderExpirationTime,
1076 );
1077 case FunctionalComponent:
1199 - return updateFunctionalComponent(current, workInProgress);
1078 + return updateFunctionalComponent(
1079 + current,
1080 + workInProgress,
1081 + renderExpirationTime,
1082 + );
1083 case ClassComponent:
1084 return updateClassComponent(
1085 current,
@@ -1222,13 +1105,13 @@ function beginWork(
1105 renderExpirationTime,
1106 );
1107 case ForwardRef:
1225 - return updateForwardRef(current, workInProgress);
1108 + return updateForwardRef(current, workInProgress, renderExpirationTime);
1109 case Fragment:
1227 - return updateFragment(current, workInProgress);
1110 + return updateFragment(current, workInProgress, renderExpirationTime);
1111 case Mode:
1229 - return updateMode(current, workInProgress);
1112 + return updateMode(current, workInProgress, renderExpirationTime);
1113 case Profiler:
1231 - return updateProfiler(current, workInProgress);
1114 + return updateProfiler(current, workInProgress, renderExpirationTime);
1115 case ContextProvider:
1116 return updateContextProvider(
1117 current,
packages/react-reconciler/src/ReactFiberClassComponent.js
+39 -23
@@ -162,7 +162,7 @@ export function applyDerivedStateFromProps(
162 // Once the update queue is empty, persist the derived state onto the
163 // base state.
164 const updateQueue = workInProgress.updateQueue;
165 - if (updateQueue !== null && updateQueue.expirationTime === NoWork) {
165 + if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
166 updateQueue.baseState = memoizedState;
167 }
168 }
@@ -183,7 +183,7 @@ const classComponentUpdater = {
183 update.callback = callback;
184 }
185
186 - enqueueUpdate(fiber, update, expirationTime);
186 + enqueueUpdate(fiber, update);
187 scheduleWork(fiber, expirationTime);
188 },
189 enqueueReplaceState(inst, payload, callback) {
@@ -202,7 +202,7 @@ const classComponentUpdater = {
202 update.callback = callback;
203 }
204
205 - enqueueUpdate(fiber, update, expirationTime);
205 + enqueueUpdate(fiber, update);
206 scheduleWork(fiber, expirationTime);
207 },
208 enqueueForceUpdate(inst, callback) {
@@ -220,7 +220,7 @@ const classComponentUpdater = {
220 update.callback = callback;
221 }
222
223 - enqueueUpdate(fiber, update, expirationTime);
223 + enqueueUpdate(fiber, update);
224 scheduleWork(fiber, expirationTime);
225 },
226 };
@@ -231,7 +231,7 @@ function checkShouldComponentUpdate(
231 newProps,
232 oldState,
233 newState,
234 - newContext,
234 + nextLegacyContext,
235 ) {
236 const instance = workInProgress.stateNode;
237 const ctor = workInProgress.type;
@@ -240,7 +240,7 @@ function checkShouldComponentUpdate(
240 const shouldUpdate = instance.shouldComponentUpdate(
241 newProps,
242 newState,
243 - newContext,
243 + nextLegacyContext,
244 );
245 stopPhaseTimer();
246
@@ -620,15 +620,15 @@ function callComponentWillReceiveProps(
620 workInProgress,
621 instance,
622 newProps,
623 - newContext,
623 + nextLegacyContext,
624 ) {
625 const oldState = instance.state;
626 startPhaseTimer(workInProgress, 'componentWillReceiveProps');
627 if (typeof instance.componentWillReceiveProps === 'function') {
628 - instance.componentWillReceiveProps(newProps, newContext);
628 + instance.componentWillReceiveProps(newProps, nextLegacyContext);
629 }
630 if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') {
631 - instance.UNSAFE_componentWillReceiveProps(newProps, newContext);
631 + instance.UNSAFE_componentWillReceiveProps(newProps, nextLegacyContext);
632 }
633 stopPhaseTimer();
634
@@ -741,6 +741,7 @@ function mountClassInstance(
741
742 function resumeMountClassInstance(
743 workInProgress: Fiber,
744 + hasPendingNewContext: boolean,
745 renderExpirationTime: ExpirationTime,
746 ): boolean {
747 const ctor = workInProgress.type;
@@ -751,8 +752,11 @@ function resumeMountClassInstance(
752 instance.props = oldProps;
753
754 const oldContext = instance.context;
754 - const newUnmaskedContext = getUnmaskedContext(workInProgress);
755 - const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
755 + const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress);
756 + const nextLegacyContext = getMaskedContext(
757 + workInProgress,
758 + nextLegacyUnmaskedContext,
759 + );
760
761 const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
762 const hasNewLifecycles =
@@ -770,12 +774,12 @@ function resumeMountClassInstance(
774 (typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
775 typeof instance.componentWillReceiveProps === 'function')
776 ) {
773 - if (oldProps !== newProps || oldContext !== newContext) {
777 + if (oldProps !== newProps || oldContext !== nextLegacyContext) {
778 callComponentWillReceiveProps(
779 workInProgress,
780 instance,
781 newProps,
778 - newContext,
782 + nextLegacyContext,
783 );
784 }
785 }
@@ -799,6 +803,7 @@ function resumeMountClassInstance(
803 oldProps === newProps &&
804 oldState === newState &&
805 !hasContextChanged() &&
806 + !hasPendingNewContext &&
807 !checkHasForceUpdateAfterProcessing()
808 ) {
809 // If an update was already in progress, we should schedule an Update
@@ -820,13 +825,14 @@ function resumeMountClassInstance(
825
826 const shouldUpdate =
827 checkHasForceUpdateAfterProcessing() ||
828 + hasPendingNewContext ||
829 checkShouldComponentUpdate(
830 workInProgress,
831 oldProps,
832 newProps,
833 oldState,
834 newState,
829 - newContext,
835 + nextLegacyContext,
836 );
837
838 if (shouldUpdate) {
@@ -866,7 +872,7 @@ function resumeMountClassInstance(
872 // if shouldComponentUpdate returns false.
873 instance.props = newProps;
874 instance.state = newState;
869 - instance.context = newContext;
875 + instance.context = nextLegacyContext;
876
877 return shouldUpdate;
878 }
@@ -875,6 +881,7 @@ function resumeMountClassInstance(
881 function updateClassInstance(
882 current: Fiber,
883 workInProgress: Fiber,
884 + hasPendingNewContext: boolean,
885 renderExpirationTime: ExpirationTime,
886 ): boolean {
887 const ctor = workInProgress.type;
@@ -885,8 +892,11 @@ function updateClassInstance(
892 instance.props = oldProps;
893
894 const oldContext = instance.context;
888 - const newUnmaskedContext = getUnmaskedContext(workInProgress);
889 - const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
895 + const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress);
896 + const nextLegacyContext = getMaskedContext(
897 + workInProgress,
898 + nextLegacyUnmaskedContext,
899 + );
900
901 const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
902 const hasNewLifecycles =
@@ -904,12 +914,12 @@ function updateClassInstance(
914 (typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
915 typeof instance.componentWillReceiveProps === 'function')
916 ) {
907 - if (oldProps !== newProps || oldContext !== newContext) {
917 + if (oldProps !== newProps || oldContext !== nextLegacyContext) {
918 callComponentWillReceiveProps(
919 workInProgress,
920 instance,
921 newProps,
912 - newContext,
922 + nextLegacyContext,
923 );
924 }
925 }
@@ -934,6 +944,7 @@ function updateClassInstance(
944 oldProps === newProps &&
945 oldState === newState &&
946 !hasContextChanged() &&
947 + !hasPendingNewContext &&
948 !checkHasForceUpdateAfterProcessing()
949 ) {
950 // If an update was already in progress, we should schedule an Update
@@ -968,13 +979,14 @@ function updateClassInstance(
979
980 const shouldUpdate =
981 checkHasForceUpdateAfterProcessing() ||
982 + hasPendingNewContext ||
983 checkShouldComponentUpdate(
984 workInProgress,
985 oldProps,
986 newProps,
987 oldState,
988 newState,
977 - newContext,
989 + nextLegacyContext,
990 );
991
992 if (shouldUpdate) {
@@ -987,10 +999,14 @@ function updateClassInstance(
999 ) {
1000 startPhaseTimer(workInProgress, 'componentWillUpdate');
1001 if (typeof instance.componentWillUpdate === 'function') {
990 - instance.componentWillUpdate(newProps, newState, newContext);
1002 + instance.componentWillUpdate(newProps, newState, nextLegacyContext);
1003 }
1004 if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
993 - instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
1005 + instance.UNSAFE_componentWillUpdate(
1006 + newProps,
1007 + newState,
1008 + nextLegacyContext,
1009 + );
1010 }
1011 stopPhaseTimer();
1012 }
@@ -1030,7 +1046,7 @@ function updateClassInstance(
1046 // if shouldComponentUpdate returns false.
1047 instance.props = newProps;
1048 instance.state = newState;
1033 - instance.context = newContext;
1049 + instance.context = nextLegacyContext;
1050
1051 return shouldUpdate;
1052 }
packages/react-reconciler/src/ReactFiberDispatcher.js new
+14
@@ -0,0 +1,14 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {readContext} from './ReactFiberNewContext';
11 +
12 +export const Dispatcher = {
13 + readContext,
14 +};
packages/react-reconciler/src/ReactFiberNewContext.js
+225 -22
@@ -7,22 +7,26 @@
7 * @flow
8 */
9
10 -import type {Fiber} from './ReactFiber';
10 import type {ReactContext} from 'shared/ReactTypes';
11 +import type {Fiber} from './ReactFiber';
12 import type {StackCursor} from './ReactFiberStack';
13 +import type {ExpirationTime} from './ReactFiberExpirationTime';
14
14 -export type NewContext = {
15 - pushProvider(providerFiber: Fiber): void,
16 - popProvider(providerFiber: Fiber): void,
17 - getContextCurrentValue(context: ReactContext<any>): any,
18 - getContextChangedBits(context: ReactContext<any>): number,
15 +export type ContextDependency<T> = {
16 + context: ReactContext<T>,
17 + observedBits: number,
18 + next: ContextDependency<mixed> | null,
19 };
20
21 import warningWithoutStack from 'shared/warningWithoutStack';
22 import {isPrimaryRenderer} from './ReactFiberHostConfig';
23 import {createCursor, push, pop} from './ReactFiberStack';
24 +import maxSigned31BitInt from './maxSigned31BitInt';
25 +import {NoWork} from './ReactFiberExpirationTime';
26 +import {ContextProvider} from 'shared/ReactTypeOfWork';
27 +
28 +import invariant from 'shared/invariant';
29
25 -const providerCursor: StackCursor<Fiber | null> = createCursor(null);
30 const valueCursor: StackCursor<mixed> = createCursor(null);
31 const changedBitsCursor: StackCursor<number> = createCursor(0);
32
@@ -32,13 +36,24 @@ if (__DEV__) {
36 rendererSigil = {};
37 }
38
35 -function pushProvider(providerFiber: Fiber): void {
39 +let currentlyRenderingFiber: Fiber | null = null;
40 +let lastContextDependency: ContextDependency<mixed> | null = null;
41 +let lastContext: ReactContext<any> | null = null;
42 +
43 +export function resetContextDependences(): void {
44 + // This is called right before React yields execution, to ensure `readContext`
45 + // cannot be called outside the render phase.
46 + currentlyRenderingFiber = null;
47 + lastContextDependency = null;
48 + lastContext = null;
49 +}
50 +
51 +export function pushProvider(providerFiber: Fiber): void {
52 const context: ReactContext<any> = providerFiber.type._context;
53
54 if (isPrimaryRenderer) {
55 push(changedBitsCursor, context._changedBits, providerFiber);
56 push(valueCursor, context._currentValue, providerFiber);
41 - push(providerCursor, providerFiber, providerFiber);
57
58 context._currentValue = providerFiber.pendingProps.value;
59 context._changedBits = providerFiber.stateNode;
@@ -55,7 +70,6 @@ function pushProvider(providerFiber: Fiber): void {
70 } else {
71 push(changedBitsCursor, context._changedBits2, providerFiber);
72 push(valueCursor, context._currentValue2, providerFiber);
58 - push(providerCursor, providerFiber, providerFiber);
73
74 context._currentValue2 = providerFiber.pendingProps.value;
75 context._changedBits2 = providerFiber.stateNode;
@@ -72,11 +86,10 @@ function pushProvider(providerFiber: Fiber): void {
86 }
87 }
88
75 -function popProvider(providerFiber: Fiber): void {
89 +export function popProvider(providerFiber: Fiber): void {
90 const changedBits = changedBitsCursor.current;
91 const currentValue = valueCursor.current;
92
79 - pop(providerCursor, providerFiber);
93 pop(valueCursor, providerFiber);
94 pop(changedBitsCursor, providerFiber);
95
@@ -90,17 +103,207 @@ function popProvider(providerFiber: Fiber): void {
103 }
104 }
105
93 -function getContextCurrentValue(context: ReactContext<any>): any {
94 - return isPrimaryRenderer ? context._currentValue : context._currentValue2;
106 +export function propagateContextChange(
107 + workInProgress: Fiber,
108 + context: ReactContext<mixed>,
109 + changedBits: number,
110 + renderExpirationTime: ExpirationTime,
111 +): void {
112 + let fiber = workInProgress.child;
113 + if (fiber !== null) {
114 + // Set the return pointer of the child to the work-in-progress fiber.
115 + fiber.return = workInProgress;
116 + }
117 + while (fiber !== null) {
118 + let nextFiber;
119 +
120 + // Visit this fiber.
121 + let dependency = fiber.firstContextDependency;
122 + if (dependency !== null) {
123 + do {
124 + // Check if the context matches.
125 + if (
126 + dependency.context === context &&
127 + (dependency.observedBits & changedBits) !== 0
128 + ) {
129 + // Match! Schedule an update on this fiber.
130 + if (
131 + fiber.expirationTime === NoWork ||
132 + fiber.expirationTime > renderExpirationTime
133 + ) {
134 + fiber.expirationTime = renderExpirationTime;
135 + }
136 + let alternate = fiber.alternate;
137 + if (
138 + alternate !== null &&
139 + (alternate.expirationTime === NoWork ||
140 + alternate.expirationTime > renderExpirationTime)
141 + ) {
142 + alternate.expirationTime = renderExpirationTime;
143 + }
144 + // Update the child expiration time of all the ancestors, including
145 + // the alternates.
146 + let node = fiber.return;
147 + while (node !== null) {
148 + alternate = node.alternate;
149 + if (
150 + node.childExpirationTime === NoWork ||
151 + node.childExpirationTime > renderExpirationTime
152 + ) {
153 + node.childExpirationTime = renderExpirationTime;
154 + if (
155 + alternate !== null &&
156 + (alternate.childExpirationTime === NoWork ||
157 + alternate.childExpirationTime > renderExpirationTime)
158 + ) {
159 + alternate.childExpirationTime = renderExpirationTime;
160 + }
161 + } else if (
162 + alternate !== null &&
163 + (alternate.childExpirationTime === NoWork ||
164 + alternate.childExpirationTime > renderExpirationTime)
165 + ) {
166 + alternate.childExpirationTime = renderExpirationTime;
167 + } else {
168 + // Neither alternate was updated, which means the rest of the
169 + // ancestor path already has sufficient priority.
170 + break;
171 + }
172 + node = node.return;
173 + }
174 + // Don't scan deeper than a matching consumer. When we render the
175 + // consumer, we'll continue scanning from that point. This way the
176 + // scanning work is time-sliced.
177 + nextFiber = null;
178 + } else {
179 + nextFiber = fiber.child;
180 + }
181 + dependency = dependency.next;
182 + } while (dependency !== null);
183 + } else if (fiber.tag === ContextProvider) {
184 + // Don't scan deeper if this is a matching provider
185 + nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
186 + } else {
187 + // Traverse down.
188 + nextFiber = fiber.child;
189 + }
190 +
191 + if (nextFiber !== null) {
192 + // Set the return pointer of the child to the work-in-progress fiber.
193 + nextFiber.return = fiber;
194 + } else {
195 + // No child. Traverse to next sibling.
196 + nextFiber = fiber;
197 + while (nextFiber !== null) {
198 + if (nextFiber === workInProgress) {
199 + // We're back to the root of this subtree. Exit.
200 + nextFiber = null;
201 + break;
202 + }
203 + let sibling = nextFiber.sibling;
204 + if (sibling !== null) {
205 + // Set the return pointer of the sibling to the work-in-progress fiber.
206 + sibling.return = nextFiber.return;
207 + nextFiber = sibling;
208 + break;
209 + }
210 + // No more siblings. Traverse up.
211 + nextFiber = nextFiber.return;
212 + }
213 + }
214 + fiber = nextFiber;
215 + }
216 }
217
97 -function getContextChangedBits(context: ReactContext<any>): number {
98 - return isPrimaryRenderer ? context._changedBits : context._changedBits2;
218 +export function prepareToReadContext(
219 + workInProgress: Fiber,
220 + renderExpirationTime: ExpirationTime,
221 +): boolean {
222 + currentlyRenderingFiber = workInProgress;
223 + lastContextDependency = null;
224 + lastContext = null;
225 +
226 + const firstContextDependency = workInProgress.firstContextDependency;
227 + if (firstContextDependency !== null) {
228 + // Reset the work-in-progress list
229 + workInProgress.firstContextDependency = null;
230 +
231 + // Iterate through the context dependencies and see if there were any
232 + // changes. If so, continue propagating the context change by scanning
233 + // the child subtree.
234 + let dependency = firstContextDependency;
235 + let hasPendingContext = false;
236 + do {
237 + const context = dependency.context;
238 + const changedBits = isPrimaryRenderer
239 + ? context._changedBits
240 + : context._changedBits2;
241 + if (changedBits !== 0) {
242 + // Resume context change propagation. We need to call this even if
243 + // this fiber bails out, in case deeply nested consumers observe more
244 + // bits than this one.
245 + propagateContextChange(
246 + workInProgress,
247 + context,
248 + changedBits,
249 + renderExpirationTime,
250 + );
251 + if ((changedBits & dependency.observedBits) !== 0) {
252 + hasPendingContext = true;
253 + }
254 + }
255 + dependency = dependency.next;
256 + } while (dependency !== null);
257 + return hasPendingContext;
258 + } else {
259 + return false;
260 + }
261 }
262
101 -export {
102 - pushProvider,
103 - popProvider,
104 - getContextCurrentValue,
105 - getContextChangedBits,
106 -};
263 +export function readContext<T>(
264 + context: ReactContext<T>,
265 + observedBits: void | number | boolean,
266 +): T {
267 + if (typeof observedBits !== 'number') {
268 + if (observedBits === false) {
269 + // Do not observe updates
270 + observedBits = 0;
271 + } else {
272 + // Observe all updates
273 + observedBits = maxSigned31BitInt;
274 + }
275 + }
276 +
277 + if (lastContext === null) {
278 + invariant(
279 + currentlyRenderingFiber !== null,
280 + 'Context.unstable_read(): Context can only be read while React is ' +
281 + 'rendering, e.g. inside the render method or getDerivedStateFromProps.',
282 + );
283 + // This is the first dependency in the list
284 + currentlyRenderingFiber.firstContextDependency = lastContextDependency = {
285 + context: ((context: any): ReactContext<mixed>),
286 + observedBits,
287 + next: null,
288 + };
289 + lastContext = context;
290 + } else {
291 + // `lastContextDependency` is always non-null if `lastContext is.
292 + const lastDependency: ContextDependency<T> = (lastContextDependency: any);
293 + if (lastContext === context) {
294 + // Fast path. The previous context has the same type. We can reuse
295 + // the same node.
296 + lastDependency.observedBits |= observedBits;
297 + } else {
298 + // Append a new context item.
299 + lastContextDependency = lastDependency.next = {
300 + context: ((context: any): ReactContext<mixed>),
301 + observedBits,
302 + next: null,
303 + };
304 + lastContext = context;
305 + }
306 + }
307 +
308 + return isPrimaryRenderer ? context._currentValue : context._currentValue2;
309 +}
packages/react-reconciler/src/ReactFiberReconciler.js
+1 -1
@@ -135,7 +135,7 @@ function scheduleRootUpdate(
135 );
136 update.callback = callback;
137 }
138 - enqueueUpdate(current, update, expirationTime);
138 + enqueueUpdate(current, update);
139
140 scheduleWork(current, expirationTime);
141 return expirationTime;
packages/react-reconciler/src/ReactFiberScheduler.js
+93 -51
@@ -109,7 +109,7 @@ import {
109 popTopLevelContextObject as popTopLevelLegacyContextObject,
110 popContextProvider as popLegacyContextProvider,
111 } from './ReactFiberContext';
112 -import {popProvider} from './ReactFiberNewContext';
112 +import {popProvider, resetContextDependences} from './ReactFiberNewContext';
113 import {popHostContext, popHostContainer} from './ReactFiberHostContext';
114 import {
115 checkActualRenderTimeStackEmpty,
@@ -145,6 +145,7 @@ import {
145 commitAttachRef,
146 commitDetachRef,
147 } from './ReactFiberCommitWork';
148 +import {Dispatcher} from './ReactFiberDispatcher';
149
150 export type Deadline = {
151 timeRemaining: () => number,
@@ -532,8 +533,15 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
533 // Update the pending priority levels to account for the work that we are
534 // about to commit. This needs to happen before calling the lifecycles, since
535 // they may schedule additional updates.
535 - const earliestRemainingTime = finishedWork.expirationTime;
536 - markCommittedPriorityLevels(root, earliestRemainingTime);
536 + const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
537 + const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
538 + const earliestRemainingTimeBeforeCommit =
539 + updateExpirationTimeBeforeCommit === NoWork ||
540 + (childExpirationTimeBeforeCommit !== NoWork &&
541 + childExpirationTimeBeforeCommit < updateExpirationTimeBeforeCommit)
542 + ? childExpirationTimeBeforeCommit
543 + : updateExpirationTimeBeforeCommit;
544 + markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit);
545
546 // Reset this to null before calling lifecycles
547 ReactCurrentOwner.current = null;
@@ -705,71 +713,85 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
713 ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
714 }
715
708 - const expirationTime = root.expirationTime;
709 - if (expirationTime === NoWork) {
716 + const updateExpirationTimeAfterCommit = finishedWork.expirationTime;
717 + const childExpirationTimeAfterCommit = finishedWork.childExpirationTime;
718 + const earliestRemainingTimeAfterCommit =
719 + updateExpirationTimeAfterCommit === NoWork ||
720 + (childExpirationTimeAfterCommit !== NoWork &&
721 + childExpirationTimeAfterCommit < updateExpirationTimeAfterCommit)
722 + ? childExpirationTimeAfterCommit
723 + : updateExpirationTimeAfterCommit;
724 + if (earliestRemainingTimeAfterCommit === NoWork) {
725 // If there's no remaining work, we can clear the set of already failed
726 // error boundaries.
727 legacyErrorBoundariesThatAlreadyFailed = null;
728 }
714 - onCommit(root, expirationTime);
729 + onCommit(root, earliestRemainingTimeAfterCommit);
730 }
731
717 -function resetExpirationTime(
732 +function resetChildExpirationTime(
733 workInProgress: Fiber,
734 renderTime: ExpirationTime,
735 ) {
721 - if (renderTime !== Never && workInProgress.expirationTime === Never) {
736 + if (renderTime !== Never && workInProgress.childExpirationTime === Never) {
737 // The children of this component are hidden. Don't bubble their
738 // expiration times.
739 return;
740 }
741
727 - // Check for pending updates.
728 - let newExpirationTime = NoWork;
729 - switch (workInProgress.tag) {
730 - case HostRoot:
731 - case ClassComponent: {
732 - const updateQueue = workInProgress.updateQueue;
733 - if (updateQueue !== null) {
734 - newExpirationTime = updateQueue.expirationTime;
735 - }
736 - }
737 - }
738 -
739 - // TODO: Calls need to visit stateNode
742 + let newChildExpirationTime = NoWork;
743
744 // Bubble up the earliest expiration time.
742 - // (And "base" render timers if that feature flag is enabled)
745 if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
746 + // We're in profiling mode. Let's use this same traversal to update the
747 + // "base" render times.
748 let treeBaseDuration = workInProgress.selfBaseDuration;
749 let child = workInProgress.child;
750 while (child !== null) {
747 - treeBaseDuration += child.treeBaseDuration;
751 + const childUpdateExpirationTime = child.expirationTime;
752 + const childChildExpirationTime = child.childExpirationTime;
753 if (
749 - child.expirationTime !== NoWork &&
750 - (newExpirationTime === NoWork ||
751 - newExpirationTime > child.expirationTime)
754 + newChildExpirationTime === NoWork ||
755 + (childUpdateExpirationTime !== NoWork &&
756 + childUpdateExpirationTime < newChildExpirationTime)
757 ) {
753 - newExpirationTime = child.expirationTime;
758 + newChildExpirationTime = childUpdateExpirationTime;
759 }
760 + if (
761 + newChildExpirationTime === NoWork ||
762 + (childChildExpirationTime !== NoWork &&
763 + childChildExpirationTime < newChildExpirationTime)
764 + ) {
765 + newChildExpirationTime = childChildExpirationTime;
766 + }
767 + treeBaseDuration += child.treeBaseDuration;
768 child = child.sibling;
769 }
770 workInProgress.treeBaseDuration = treeBaseDuration;
771 } else {
772 let child = workInProgress.child;
773 while (child !== null) {
774 + const childUpdateExpirationTime = child.expirationTime;
775 + const childChildExpirationTime = child.childExpirationTime;
776 if (
762 - child.expirationTime !== NoWork &&
763 - (newExpirationTime === NoWork ||
764 - newExpirationTime > child.expirationTime)
777 + newChildExpirationTime === NoWork ||
778 + (childUpdateExpirationTime !== NoWork &&
779 + childUpdateExpirationTime < newChildExpirationTime)
780 ) {
766 - newExpirationTime = child.expirationTime;
781 + newChildExpirationTime = childUpdateExpirationTime;
782 + }
783 + if (
784 + newChildExpirationTime === NoWork ||
785 + (childChildExpirationTime !== NoWork &&
786 + childChildExpirationTime < newChildExpirationTime)
787 + ) {
788 + newChildExpirationTime = childChildExpirationTime;
789 }
790 child = child.sibling;
791 }
792 }
793
772 - workInProgress.expirationTime = newExpirationTime;
794 + workInProgress.childExpirationTime = newChildExpirationTime;
795 }
796
797 function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
@@ -798,7 +820,7 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
820 );
821 let next = nextUnitOfWork;
822 stopWorkTimer(workInProgress);
801 - resetExpirationTime(workInProgress, nextRenderExpirationTime);
823 + resetChildExpirationTime(workInProgress, nextRenderExpirationTime);
824 if (__DEV__) {
825 ReactCurrentFiber.resetCurrentFiber();
826 }
@@ -1017,6 +1039,7 @@ function renderRoot(
1039 'by a bug in React. Please file an issue.',
1040 );
1041 isWorking = true;
1042 + ReactCurrentOwner.currentDispatcher = Dispatcher;
1043
1044 const expirationTime = root.nextExpirationTimeToWorkOn;
1045
@@ -1107,6 +1130,8 @@ function renderRoot(
1130
1131 // We're done performing work. Time to clean up.
1132 isWorking = false;
1133 + ReactCurrentOwner.currentDispatcher = null;
1134 + resetContextDependences();
1135
1136 // Yield back to main thread.
1137 if (didFatal) {
@@ -1267,7 +1292,7 @@ function dispatch(
1292 errorInfo,
1293 expirationTime,
1294 );
1270 - enqueueUpdate(fiber, update, expirationTime);
1295 + enqueueUpdate(fiber, update);
1296 scheduleWork(fiber, expirationTime);
1297 return;
1298 }
@@ -1275,7 +1300,7 @@ function dispatch(
1300 case HostRoot: {
1301 const errorInfo = createCapturedValue(value, sourceFiber);
1302 const update = createRootErrorUpdate(fiber, errorInfo, expirationTime);
1278 - enqueueUpdate(fiber, update, expirationTime);
1303 + enqueueUpdate(fiber, update);
1304 scheduleWork(fiber, expirationTime);
1305 return;
1306 }
@@ -1289,7 +1314,7 @@ function dispatch(
1314 const rootFiber = sourceFiber;
1315 const errorInfo = createCapturedValue(value, rootFiber);
1316 const update = createRootErrorUpdate(rootFiber, errorInfo, expirationTime);
1292 - enqueueUpdate(rootFiber, update, expirationTime);
1317 + enqueueUpdate(rootFiber, update);
1318 scheduleWork(rootFiber, expirationTime);
1319 }
1320 }
@@ -1408,35 +1433,52 @@ function retrySuspendedRoot(
1433 }
1434
1435 function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
1411 - // Walk the parent path to the root and update each node's
1412 - // expiration time.
1413 - let node = fiber;
1414 - do {
1415 - const alternate = node.alternate;
1436 + // Update the source fiber's expiration time
1437 + if (
1438 + fiber.expirationTime === NoWork ||
1439 + fiber.expirationTime > expirationTime
1440 + ) {
1441 + fiber.expirationTime = expirationTime;
1442 + }
1443 + let alternate = fiber.alternate;
1444 + if (
1445 + alternate !== null &&
1446 + (alternate.expirationTime === NoWork ||
1447 + alternate.expirationTime > expirationTime)
1448 + ) {
1449 + alternate.expirationTime = expirationTime;
1450 + }
1451 + // Walk the parent path to the root and update the child expiration time.
1452 + let node = fiber.return;
1453 + if (node === null && fiber.tag === HostRoot) {
1454 + return fiber.stateNode;
1455 + }
1456 + while (node !== null) {
1457 + alternate = node.alternate;
1458 if (
1417 - node.expirationTime === NoWork ||
1418 - node.expirationTime > expirationTime
1459 + node.childExpirationTime === NoWork ||
1460 + node.childExpirationTime > expirationTime
1461 ) {
1420 - node.expirationTime = expirationTime;
1462 + node.childExpirationTime = expirationTime;
1463 if (
1464 alternate !== null &&
1423 - (alternate.expirationTime === NoWork ||
1424 - alternate.expirationTime > expirationTime)
1465 + (alternate.childExpirationTime === NoWork ||
1466 + alternate.childExpirationTime > expirationTime)
1467 ) {
1426 - alternate.expirationTime = expirationTime;
1468 + alternate.childExpirationTime = expirationTime;
1469 }
1470 } else if (
1471 alternate !== null &&
1430 - (alternate.expirationTime === NoWork ||
1431 - alternate.expirationTime > expirationTime)
1472 + (alternate.childExpirationTime === NoWork ||
1473 + alternate.childExpirationTime > expirationTime)
1474 ) {
1433 - alternate.expirationTime = expirationTime;
1475 + alternate.childExpirationTime = expirationTime;
1476 }
1477 if (node.return === null && node.tag === HostRoot) {
1478 return node.stateNode;
1479 }
1480 node = node.return;
1439 - } while (node !== null);
1481 + }
1482 return null;
1483 }
1484
packages/react-reconciler/src/ReactFiberUnwindWork.js
+7 -4
@@ -70,7 +70,7 @@ import {
70 LOW_PRIORITY_EXPIRATION,
71 } from './ReactFiberExpirationTime';
72 import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority';
73 -import {reconcileChildrenAtExpirationTime} from './ReactFiberBeginWork';
73 +import {reconcileChildren} from './ReactFiberBeginWork';
74
75 function NoopComponent() {
76 return null;
@@ -238,7 +238,7 @@ function throwException(
238
239 // Unmount the source fiber's children
240 const nextChildren = null;
241 - reconcileChildrenAtExpirationTime(
241 + reconcileChildren(
242 sourceFiber.alternate,
243 sourceFiber,
244 nextChildren,
@@ -310,6 +310,7 @@ function throwException(
310 renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime);
311
312 workInProgress.effectTag |= ShouldCapture;
313 + workInProgress.expirationTime = renderExpirationTime;
314 return;
315 }
316 // This boundary already captured during this render. Continue to the
@@ -334,12 +335,13 @@ function throwException(
335 case HostRoot: {
336 const errorInfo = value;
337 workInProgress.effectTag |= ShouldCapture;
338 + workInProgress.expirationTime = renderExpirationTime;
339 const update = createRootErrorUpdate(
340 workInProgress,
341 errorInfo,
342 renderExpirationTime,
343 );
342 - enqueueCapturedUpdate(workInProgress, update, renderExpirationTime);
344 + enqueueCapturedUpdate(workInProgress, update);
345 return;
346 }
347 case ClassComponent:
@@ -356,13 +358,14 @@ function throwException(
358 !isAlreadyFailedLegacyErrorBoundary(instance)))
359 ) {
360 workInProgress.effectTag |= ShouldCapture;
361 + workInProgress.expirationTime = renderExpirationTime;
362 // Schedule the error boundary to re-render using updated state
363 const update = createClassErrorUpdate(
364 workInProgress,
365 errorInfo,
366 renderExpirationTime,
367 );
365 - enqueueCapturedUpdate(workInProgress, update, renderExpirationTime);
368 + enqueueCapturedUpdate(workInProgress, update);
369 return;
370 }
371 break;
packages/react-reconciler/src/ReactUpdateQueue.js
+13 -39
@@ -117,7 +117,6 @@ export type Update<State> = {
117 };
118
119 export type UpdateQueue<State> = {
120 - expirationTime: ExpirationTime,
120 baseState: State,
121
122 firstUpdate: Update<State> | null,
@@ -156,7 +155,6 @@ if (__DEV__) {
155
156 export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
157 const queue: UpdateQueue<State> = {
159 - expirationTime: NoWork,
158 baseState,
159 firstUpdate: null,
160 lastUpdate: null,
@@ -174,7 +172,6 @@ function cloneUpdateQueue<State>(
172 currentQueue: UpdateQueue<State>,
173 ): UpdateQueue<State> {
174 const queue: UpdateQueue<State> = {
177 - expirationTime: currentQueue.expirationTime,
175 baseState: currentQueue.baseState,
176 firstUpdate: currentQueue.firstUpdate,
177 lastUpdate: currentQueue.lastUpdate,
@@ -209,7 +206,6 @@ export function createUpdate(expirationTime: ExpirationTime): Update<*> {
206 function appendUpdateToQueue<State>(
207 queue: UpdateQueue<State>,
208 update: Update<State>,
212 - expirationTime: ExpirationTime,
209 ) {
210 // Append the update to the end of the list.
211 if (queue.lastUpdate === null) {
@@ -219,21 +215,9 @@ function appendUpdateToQueue<State>(
215 queue.lastUpdate.next = update;
216 queue.lastUpdate = update;
217 }
222 - if (
223 - queue.expirationTime === NoWork ||
224 - queue.expirationTime > expirationTime
225 - ) {
226 - // The incoming update has the earliest expiration of any update in the
227 - // queue. Update the queue's expiration time.
228 - queue.expirationTime = expirationTime;
229 - }
218 }
219
232 -export function enqueueUpdate<State>(
233 - fiber: Fiber,
234 - update: Update<State>,
235 - expirationTime: ExpirationTime,
236 -) {
220 +export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
221 // Update queues are created lazily.
222 const alternate = fiber.alternate;
223 let queue1;
@@ -271,19 +255,19 @@ export function enqueueUpdate<State>(
255 }
256 if (queue2 === null || queue1 === queue2) {
257 // There's only a single queue.
274 - appendUpdateToQueue(queue1, update, expirationTime);
258 + appendUpdateToQueue(queue1, update);
259 } else {
260 // There are two queues. We need to append the update to both queues,
261 // while accounting for the persistent structure of the list — we don't
262 // want the same update to be added multiple times.
263 if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
264 // One of the queues is not empty. We must add the update to both queues.
281 - appendUpdateToQueue(queue1, update, expirationTime);
282 - appendUpdateToQueue(queue2, update, expirationTime);
265 + appendUpdateToQueue(queue1, update);
266 + appendUpdateToQueue(queue2, update);
267 } else {
268 // Both queues are non-empty. The last update is the same in both lists,
269 // because of structural sharing. So, only append to one of the lists.
286 - appendUpdateToQueue(queue1, update, expirationTime);
270 + appendUpdateToQueue(queue1, update);
271 // But we still need to update the `lastUpdate` pointer of queue2.
272 queue2.lastUpdate = update;
273 }
@@ -311,7 +295,6 @@ export function enqueueUpdate<State>(
295 export function enqueueCapturedUpdate<State>(
296 workInProgress: Fiber,
297 update: Update<State>,
314 - renderExpirationTime: ExpirationTime,
298 ) {
299 // Captured updates go into a separate list, and only on the work-in-
300 // progress queue.
@@ -338,14 +321,6 @@ export function enqueueCapturedUpdate<State>(
321 workInProgressQueue.lastCapturedUpdate.next = update;
322 workInProgressQueue.lastCapturedUpdate = update;
323 }
341 - if (
342 - workInProgressQueue.expirationTime === NoWork ||
343 - workInProgressQueue.expirationTime > renderExpirationTime
344 - ) {
345 - // The incoming update has the earliest expiration of any update in the
346 - // queue. Update the queue's expiration time.
347 - workInProgressQueue.expirationTime = renderExpirationTime;
348 - }
324 }
325
326 function ensureWorkInProgressQueueIsAClone<State>(
@@ -438,14 +413,6 @@ export function processUpdateQueue<State>(
413 ): void {
414 hasForceUpdate = false;
415
441 - if (
442 - queue.expirationTime === NoWork ||
443 - queue.expirationTime > renderExpirationTime
444 - ) {
445 - // Insufficient priority. Bailout.
446 - return;
447 - }
448 -
416 queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);
417
418 if (__DEV__) {
@@ -577,8 +544,15 @@ export function processUpdateQueue<State>(
544 queue.baseState = newBaseState;
545 queue.firstUpdate = newFirstUpdate;
546 queue.firstCapturedUpdate = newFirstCapturedUpdate;
580 - queue.expirationTime = newExpirationTime;
547
548 + // Set the remaining expiration time to be whatever is remaining in the queue.
549 + // This should be fine because the only two other things that contribute to
550 + // expiration time are props and context. We're already in the middle of the
551 + // begin phase by the time we start processing the queue, so we've already
552 + // dealt with the props. Context in components that specify
553 + // shouldComponentUpdate is tricky; but we'll have to account for
554 + // that regardless.
555 + workInProgress.expirationTime = newExpirationTime;
556 workInProgress.memoizedState = resultState;
557
558 if (__DEV__) {
packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
+1147 -952
@@ -30,1102 +30,1297 @@ describe('ReactNewContext', () => {
30 // return {type: 'div', children, prop: undefined};
31 // }
32
33 + function Text(props) {
34 + ReactNoop.yield(props.text);
35 + return <span prop={props.text} />;
36 + }
37 +
38 function span(prop) {
39 return {type: 'span', children: [], prop};
40 }
41
37 - it('simple mount and update', () => {
38 - const Context = React.createContext(1);
39 -
40 - function Consumer(props) {
41 - return (
42 - <Context.Consumer>
43 - {value => <span prop={'Result: ' + value} />}
44 - </Context.Consumer>
45 - );
46 - }
47 -
48 - const Indirection = React.Fragment;
49 -
50 - function App(props) {
51 - return (
52 - <Context.Provider value={props.value}>
53 - <Indirection>
54 - <Indirection>
55 - <Consumer />
56 - </Indirection>
57 - </Indirection>
58 - </Context.Provider>
59 - );
60 - }
61 -
62 - ReactNoop.render(<App value={2} />);
63 - ReactNoop.flush();
64 - expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
65 -
66 - // Update
67 - ReactNoop.render(<App value={3} />);
68 - ReactNoop.flush();
69 - expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
70 - });
42 + // We have several ways of reading from context. sharedContextTests runs
43 + // a suite of tests for a given context consumer implementation.
44 + sharedContextTests('Context.Consumer', Context => Context.Consumer);
45 + sharedContextTests(
46 + 'Context.unstable_read inside functional component',
47 + Context =>
48 + function Consumer(props) {
49 + const observedBits = props.unstable_observedBits;
50 + const contextValue = Context.unstable_read(observedBits);
51 + const render = props.children;
52 + return render(contextValue);
53 + },
54 + );
55 + sharedContextTests(
56 + 'Context.unstable_read inside class component',
57 + Context =>
58 + class Consumer extends React.Component {
59 + render() {
60 + const observedBits = this.props.unstable_observedBits;
61 + const contextValue = Context.unstable_read(observedBits);
62 + const render = this.props.children;
63 + return render(contextValue);
64 + }
65 + },
66 + );
67
72 - it('propagates through shouldComponentUpdate false', () => {
73 - const Context = React.createContext(1);
68 + function sharedContextTests(label, getConsumer) {
69 + describe(`reading context with ${label}`, () => {
70 + it('simple mount and update', () => {
71 + const Context = React.createContext(1);
72 + const Consumer = getConsumer(Context);
73
75 - function Provider(props) {
76 - ReactNoop.yield('Provider');
77 - return (
78 - <Context.Provider value={props.value}>
79 - {props.children}
80 - </Context.Provider>
81 - );
82 - }
74 + const Indirection = React.Fragment;
75
84 - function Consumer(props) {
85 - ReactNoop.yield('Consumer');
86 - return (
87 - <Context.Consumer>
88 - {value => {
89 - ReactNoop.yield('Consumer render prop');
90 - return <span prop={'Result: ' + value} />;
91 - }}
92 - </Context.Consumer>
93 - );
94 - }
76 + function App(props) {
77 + return (
78 + <Context.Provider value={props.value}>
79 + <Indirection>
80 + <Indirection>
81 + <Consumer>
82 + {value => <span prop={'Result: ' + value} />}
83 + </Consumer>
84 + </Indirection>
85 + </Indirection>
86 + </Context.Provider>
87 + );
88 + }
89
96 - class Indirection extends React.Component {
97 - shouldComponentUpdate() {
98 - return false;
99 - }
100 - render() {
101 - ReactNoop.yield('Indirection');
102 - return this.props.children;
103 - }
104 - }
90 + ReactNoop.render(<App value={2} />);
91 + ReactNoop.flush();
92 + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
93
106 - function App(props) {
107 - ReactNoop.yield('App');
108 - return (
109 - <Provider value={props.value}>
110 - <Indirection>
111 - <Indirection>
112 - <Consumer />
113 - </Indirection>
114 - </Indirection>
115 - </Provider>
116 - );
117 - }
94 + // Update
95 + ReactNoop.render(<App value={3} />);
96 + ReactNoop.flush();
97 + expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
98 + });
99
119 - ReactNoop.render(<App value={2} />);
120 - expect(ReactNoop.flush()).toEqual([
121 - 'App',
122 - 'Provider',
123 - 'Indirection',
124 - 'Indirection',
125 - 'Consumer',
126 - 'Consumer render prop',
127 - ]);
128 - expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
129 -
130 - // Update
131 - ReactNoop.render(<App value={3} />);
132 - expect(ReactNoop.flush()).toEqual([
133 - 'App',
134 - 'Provider',
135 - 'Consumer render prop',
136 - ]);
137 - expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
138 - });
100 + it('propagates through shouldComponentUpdate false', () => {
101 + const Context = React.createContext(1);
102 + const ContextConsumer = getConsumer(Context);
103
140 - it('consumers bail out if context value is the same', () => {
141 - const Context = React.createContext(1);
104 + function Provider(props) {
105 + ReactNoop.yield('Provider');
106 + return (
107 + <Context.Provider value={props.value}>
108 + {props.children}
109 + </Context.Provider>
110 + );
111 + }
112
143 - function Provider(props) {
144 - ReactNoop.yield('Provider');
145 - return (
146 - <Context.Provider value={props.value}>
147 - {props.children}
148 - </Context.Provider>
149 - );
150 - }
113 + function Consumer(props) {
114 + ReactNoop.yield('Consumer');
115 + return (
116 + <ContextConsumer>
117 + {value => {
118 + ReactNoop.yield('Consumer render prop');
119 + return <span prop={'Result: ' + value} />;
120 + }}
121 + </ContextConsumer>
122 + );
123 + }
124
152 - function Consumer(props) {
153 - ReactNoop.yield('Consumer');
154 - return (
155 - <Context.Consumer>
156 - {value => {
157 - ReactNoop.yield('Consumer render prop');
158 - return <span prop={'Result: ' + value} />;
159 - }}
160 - </Context.Consumer>
161 - );
162 - }
125 + class Indirection extends React.Component {
126 + shouldComponentUpdate() {
127 + return false;
128 + }
129 + render() {
130 + ReactNoop.yield('Indirection');
131 + return this.props.children;
132 + }
133 + }
134
164 - class Indirection extends React.Component {
165 - shouldComponentUpdate() {
166 - return false;
167 - }
168 - render() {
169 - ReactNoop.yield('Indirection');
170 - return this.props.children;
171 - }
172 - }
135 + function App(props) {
136 + ReactNoop.yield('App');
137 + return (
138 + <Provider value={props.value}>
139 + <Indirection>
140 + <Indirection>
141 + <Consumer />
142 + </Indirection>
143 + </Indirection>
144 + </Provider>
145 + );
146 + }
147
174 - function App(props) {
175 - ReactNoop.yield('App');
176 - return (
177 - <Provider value={props.value}>
178 - <Indirection>
179 - <Indirection>
180 - <Consumer />
181 - </Indirection>
182 - </Indirection>
183 - </Provider>
184 - );
185 - }
148 + ReactNoop.render(<App value={2} />);
149 + expect(ReactNoop.flush()).toEqual([
150 + 'App',
151 + 'Provider',
152 + 'Indirection',
153 + 'Indirection',
154 + 'Consumer',
155 + 'Consumer render prop',
156 + ]);
157 + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
158 +
159 + // Update
160 + ReactNoop.render(<App value={3} />);
161 + expect(ReactNoop.flush()).toEqual([
162 + 'App',
163 + 'Provider',
164 + 'Consumer render prop',
165 + ]);
166 + expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
167 + });
168 +
169 + it('consumers bail out if context value is the same', () => {
170 + const Context = React.createContext(1);
171 + const ContextConsumer = getConsumer(Context);
172 +
173 + function Provider(props) {
174 + ReactNoop.yield('Provider');
175 + return (
176 + <Context.Provider value={props.value}>
177 + {props.children}
178 + </Context.Provider>
179 + );
180 + }
181
187 - ReactNoop.render(<App value={2} />);
188 - expect(ReactNoop.flush()).toEqual([
189 - 'App',
190 - 'Provider',
191 - 'Indirection',
192 - 'Indirection',
193 - 'Consumer',
194 - 'Consumer render prop',
195 - ]);
196 - expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
197 -
198 - // Update with the same context value
199 - ReactNoop.render(<App value={2} />);
200 - expect(ReactNoop.flush()).toEqual([
201 - 'App',
202 - 'Provider',
203 - // Don't call render prop again
204 - ]);
205 - expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
206 - });
182 + function Consumer(props) {
183 + ReactNoop.yield('Consumer');
184 + return (
185 + <ContextConsumer>
186 + {value => {
187 + ReactNoop.yield('Consumer render prop');
188 + return <span prop={'Result: ' + value} />;
189 + }}
190 + </ContextConsumer>
191 + );
192 + }
193
208 - it('nested providers', () => {
209 - const Context = React.createContext(1);
194 + class Indirection extends React.Component {
195 + shouldComponentUpdate() {
196 + return false;
197 + }
198 + render() {
199 + ReactNoop.yield('Indirection');
200 + return this.props.children;
201 + }
202 + }
203
211 - function Provider(props) {
212 - return (
213 - <Context.Consumer>
214 - {contextValue => (
215 - // Multiply previous context value by 2, unless prop overrides
216 - <Context.Provider value={props.value || contextValue * 2}>
217 - {props.children}
218 - </Context.Provider>
219 - )}
220 - </Context.Consumer>
221 - );
222 - }
204 + function App(props) {
205 + ReactNoop.yield('App');
206 + return (
207 + <Provider value={props.value}>
208 + <Indirection>
209 + <Indirection>
210 + <Consumer />
211 + </Indirection>
212 + </Indirection>
213 + </Provider>
214 + );
215 + }
216
224 - function Consumer(props) {
225 - return (
226 - <Context.Consumer>
227 - {value => <span prop={'Result: ' + value} />}
228 - </Context.Consumer>
229 - );
230 - }
217 + ReactNoop.render(<App value={2} />);
218 + expect(ReactNoop.flush()).toEqual([
219 + 'App',
220 + 'Provider',
221 + 'Indirection',
222 + 'Indirection',
223 + 'Consumer',
224 + 'Consumer render prop',
225 + ]);
226 + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
227 +
228 + // Update with the same context value
229 + ReactNoop.render(<App value={2} />);
230 + expect(ReactNoop.flush()).toEqual([
231 + 'App',
232 + 'Provider',
233 + // Don't call render prop again
234 + ]);
235 + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
236 + });
237 +
238 + it('nested providers', () => {
239 + const Context = React.createContext(1);
240 + const Consumer = getConsumer(Context);
241 +
242 + function Provider(props) {
243 + return (
244 + <Consumer>
245 + {contextValue => (
246 + // Multiply previous context value by 2, unless prop overrides
247 + <Context.Provider value={props.value || contextValue * 2}>
248 + {props.children}
249 + </Context.Provider>
250 + )}
251 + </Consumer>
252 + );
253 + }
254
232 - class Indirection extends React.Component {
233 - shouldComponentUpdate() {
234 - return false;
235 - }
236 - render() {
237 - return this.props.children;
238 - }
239 - }
255 + class Indirection extends React.Component {
256 + shouldComponentUpdate() {
257 + return false;
258 + }
259 + render() {
260 + return this.props.children;
261 + }
262 + }
263
241 - function App(props) {
242 - return (
243 - <Provider value={props.value}>
244 - <Indirection>
245 - <Provider>
264 + function App(props) {
265 + return (
266 + <Provider value={props.value}>
267 <Indirection>
268 <Provider>
269 <Indirection>
249 - <Consumer />
270 + <Provider>
271 + <Indirection>
272 + <Consumer>
273 + {value => <span prop={'Result: ' + value} />}
274 + </Consumer>
275 + </Indirection>
276 + </Provider>
277 </Indirection>
278 </Provider>
279 </Indirection>
280 </Provider>
254 - </Indirection>
255 - </Provider>
256 - );
257 - }
281 + );
282 + }
283
259 - ReactNoop.render(<App value={2} />);
260 - ReactNoop.flush();
261 - expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]);
284 + ReactNoop.render(<App value={2} />);
285 + ReactNoop.flush();
286 + expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]);
287
263 - // Update
264 - ReactNoop.render(<App value={3} />);
265 - ReactNoop.flush();
266 - expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]);
267 - });
288 + // Update
289 + ReactNoop.render(<App value={3} />);
290 + ReactNoop.flush();
291 + expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]);
292 + });
293
269 - it('should provide the correct (default) values to consumers outside of a provider', () => {
270 - const FooContext = React.createContext({value: 'foo-initial'});
271 - const BarContext = React.createContext({value: 'bar-initial'});
294 + it('should provide the correct (default) values to consumers outside of a provider', () => {
295 + const FooContext = React.createContext({value: 'foo-initial'});
296 + const BarContext = React.createContext({value: 'bar-initial'});
297 + const FooConsumer = getConsumer(FooContext);
298 + const BarConsumer = getConsumer(BarContext);
299
273 - const Verify = ({actual, expected}) => {
274 - expect(expected).toBe(actual);
275 - return null;
276 - };
300 + const Verify = ({actual, expected}) => {
301 + expect(expected).toBe(actual);
302 + return null;
303 + };
304
278 - ReactNoop.render(
279 - <React.Fragment>
280 - <BarContext.Provider value={{value: 'bar-updated'}}>
281 - <BarContext.Consumer>
282 - {({value}) => <Verify actual={value} expected="bar-updated" />}
283 - </BarContext.Consumer>
284 -
285 - <FooContext.Provider value={{value: 'foo-updated'}}>
286 - <FooContext.Consumer>
287 - {({value}) => <Verify actual={value} expected="foo-updated" />}
288 - </FooContext.Consumer>
289 - </FooContext.Provider>
290 - </BarContext.Provider>
291 -
292 - <FooContext.Consumer>
293 - {({value}) => <Verify actual={value} expected="foo-initial" />}
294 - </FooContext.Consumer>
295 - <BarContext.Consumer>
296 - {({value}) => <Verify actual={value} expected="bar-initial" />}
297 - </BarContext.Consumer>
298 - </React.Fragment>,
299 - );
300 - ReactNoop.flush();
301 - });
305 + ReactNoop.render(
306 + <React.Fragment>
307 + <BarContext.Provider value={{value: 'bar-updated'}}>
308 + <BarConsumer>
309 + {({value}) => <Verify actual={value} expected="bar-updated" />}
310 + </BarConsumer>
311 +
312 + <FooContext.Provider value={{value: 'foo-updated'}}>
313 + <FooConsumer>
314 + {({value}) => (
315 + <Verify actual={value} expected="foo-updated" />
316 + )}
317 + </FooConsumer>
318 + </FooContext.Provider>
319 + </BarContext.Provider>
320 +
321 + <FooConsumer>
322 + {({value}) => <Verify actual={value} expected="foo-initial" />}
323 + </FooConsumer>
324 + <BarConsumer>
325 + {({value}) => <Verify actual={value} expected="bar-initial" />}
326 + </BarConsumer>
327 + </React.Fragment>,
328 + );
329 + ReactNoop.flush();
330 + });
331 +
332 + it('multiple consumers in different branches', () => {
333 + const Context = React.createContext(1);
334 + const Consumer = getConsumer(Context);
335 +
336 + function Provider(props) {
337 + return (
338 + <Context.Consumer>
339 + {contextValue => (
340 + // Multiply previous context value by 2, unless prop overrides
341 + <Context.Provider value={props.value || contextValue * 2}>
342 + {props.children}
343 + </Context.Provider>
344 + )}
345 + </Context.Consumer>
346 + );
347 + }
348
303 - it('multiple consumers in different branches', () => {
304 - const Context = React.createContext(1);
349 + class Indirection extends React.Component {
350 + shouldComponentUpdate() {
351 + return false;
352 + }
353 + render() {
354 + return this.props.children;
355 + }
356 + }
357
306 - function Provider(props) {
307 - return (
308 - <Context.Consumer>
309 - {contextValue => (
310 - // Multiply previous context value by 2, unless prop overrides
311 - <Context.Provider value={props.value || contextValue * 2}>
312 - {props.children}
313 - </Context.Provider>
314 - )}
315 - </Context.Consumer>
316 - );
317 - }
358 + function App(props) {
359 + return (
360 + <Provider value={props.value}>
361 + <Indirection>
362 + <Indirection>
363 + <Provider>
364 + <Consumer>
365 + {value => <span prop={'Result: ' + value} />}
366 + </Consumer>
367 + </Provider>
368 + </Indirection>
369 + <Indirection>
370 + <Consumer>
371 + {value => <span prop={'Result: ' + value} />}
372 + </Consumer>
373 + </Indirection>
374 + </Indirection>
375 + </Provider>
376 + );
377 + }
378
319 - function Consumer(props) {
320 - return (
321 - <Context.Consumer>
322 - {value => <span prop={'Result: ' + value} />}
323 - </Context.Consumer>
324 - );
325 - }
379 + ReactNoop.render(<App value={2} />);
380 + ReactNoop.flush();
381 + expect(ReactNoop.getChildren()).toEqual([
382 + span('Result: 4'),
383 + span('Result: 2'),
384 + ]);
385
327 - class Indirection extends React.Component {
328 - shouldComponentUpdate() {
329 - return false;
330 - }
331 - render() {
332 - return this.props.children;
333 - }
334 - }
386 + // Update
387 + ReactNoop.render(<App value={3} />);
388 + ReactNoop.flush();
389 + expect(ReactNoop.getChildren()).toEqual([
390 + span('Result: 6'),
391 + span('Result: 3'),
392 + ]);
393
336 - function App(props) {
337 - return (
338 - <Provider value={props.value}>
339 - <Indirection>
340 - <Indirection>
341 - <Provider>
342 - <Consumer />
343 - </Provider>
344 - </Indirection>
345 - <Indirection>
346 - <Consumer />
347 - </Indirection>
348 - </Indirection>
349 - </Provider>
350 - );
351 - }
394 + // Another update
395 + ReactNoop.render(<App value={4} />);
396 + ReactNoop.flush();
397 + expect(ReactNoop.getChildren()).toEqual([
398 + span('Result: 8'),
399 + span('Result: 4'),
400 + ]);
401 + });
402 +
403 + it('compares context values with Object.is semantics', () => {
404 + const Context = React.createContext(1);
405 + const ContextConsumer = getConsumer(Context);
406 +
407 + function Provider(props) {
408 + ReactNoop.yield('Provider');
409 + return (
410 + <Context.Provider value={props.value}>
411 + {props.children}
412 + </Context.Provider>
413 + );
414 + }
415
353 - ReactNoop.render(<App value={2} />);
354 - ReactNoop.flush();
355 - expect(ReactNoop.getChildren()).toEqual([
356 - span('Result: 4'),
357 - span('Result: 2'),
358 - ]);
416 + function Consumer(props) {
417 + ReactNoop.yield('Consumer');
418 + return (
419 + <ContextConsumer>
420 + {value => {
421 + ReactNoop.yield('Consumer render prop');
422 + return <span prop={'Result: ' + value} />;
423 + }}
424 + </ContextConsumer>
425 + );
426 + }
427
360 - // Update
361 - ReactNoop.render(<App value={3} />);
362 - ReactNoop.flush();
363 - expect(ReactNoop.getChildren()).toEqual([
364 - span('Result: 6'),
365 - span('Result: 3'),
366 - ]);
428 + class Indirection extends React.Component {
429 + shouldComponentUpdate() {
430 + return false;
431 + }
432 + render() {
433 + ReactNoop.yield('Indirection');
434 + return this.props.children;
435 + }
436 + }
437
368 - // Another update
369 - ReactNoop.render(<App value={4} />);
370 - ReactNoop.flush();
371 - expect(ReactNoop.getChildren()).toEqual([
372 - span('Result: 8'),
373 - span('Result: 4'),
374 - ]);
375 - });
438 + function App(props) {
439 + ReactNoop.yield('App');
440 + return (
441 + <Provider value={props.value}>
442 + <Indirection>
443 + <Indirection>
444 + <Consumer />
445 + </Indirection>
446 + </Indirection>
447 + </Provider>
448 + );
449 + }
450
377 - it('compares context values with Object.is semantics', () => {
378 - const Context = React.createContext(1);
451 + ReactNoop.render(<App value={NaN} />);
452 + expect(ReactNoop.flush()).toEqual([
453 + 'App',
454 + 'Provider',
455 + 'Indirection',
456 + 'Indirection',
457 + 'Consumer',
458 + 'Consumer render prop',
459 + ]);
460 + expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
461 +
462 + // Update
463 + ReactNoop.render(<App value={NaN} />);
464 + expect(ReactNoop.flush()).toEqual([
465 + 'App',
466 + 'Provider',
467 + // Consumer should not re-render again
468 + // 'Consumer render prop',
469 + ]);
470 + expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
471 + });
472 +
473 + it('context unwinds when interrupted', () => {
474 + const Context = React.createContext('Default');
475 + const ContextConsumer = getConsumer(Context);
476 +
477 + function Consumer(props) {
478 + return (
479 + <ContextConsumer>
480 + {value => <span prop={'Result: ' + value} />}
481 + </ContextConsumer>
482 + );
483 + }
484
380 - function Provider(props) {
381 - ReactNoop.yield('Provider');
382 - return (
383 - <Context.Provider value={props.value}>
384 - {props.children}
385 - </Context.Provider>
386 - );
387 - }
485 + function BadRender() {
486 + throw new Error('Bad render');
487 + }
488
389 - function Consumer(props) {
390 - ReactNoop.yield('Consumer');
391 - return (
392 - <Context.Consumer>
393 - {value => {
394 - ReactNoop.yield('Consumer render prop');
395 - return <span prop={'Result: ' + value} />;
396 - }}
397 - </Context.Consumer>
398 - );
399 - }
489 + class ErrorBoundary extends React.Component {
490 + state = {error: null};
491 + componentDidCatch(error) {
492 + this.setState({error});
493 + }
494 + render() {
495 + if (this.state.error) {
496 + return null;
497 + }
498 + return this.props.children;
499 + }
500 + }
501
401 - class Indirection extends React.Component {
402 - shouldComponentUpdate() {
403 - return false;
404 - }
405 - render() {
406 - ReactNoop.yield('Indirection');
407 - return this.props.children;
408 - }
409 - }
502 + function App(props) {
503 + return (
504 + <React.Fragment>
505 + <Context.Provider value="Does not unwind">
506 + <ErrorBoundary>
507 + <Context.Provider value="Unwinds after BadRender throws">
508 + <BadRender />
509 + </Context.Provider>
510 + </ErrorBoundary>
511 + <Consumer />
512 + </Context.Provider>
513 + </React.Fragment>
514 + );
515 + }
516
411 - function App(props) {
412 - ReactNoop.yield('App');
413 - return (
414 - <Provider value={props.value}>
415 - <Indirection>
416 - <Indirection>
417 - <Consumer />
418 - </Indirection>
419 - </Indirection>
420 - </Provider>
421 - );
422 - }
517 + ReactNoop.render(<App value="A" />);
518 + ReactNoop.flush();
519 + expect(ReactNoop.getChildren()).toEqual([
520 + // The second provider should use the default value.
521 + span('Result: Does not unwind'),
522 + ]);
523 + });
524 +
525 + it('can skip consumers with bitmask', () => {
526 + const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
527 + let result = 0;
528 + if (a.foo !== b.foo) {
529 + result |= 0b01;
530 + }
531 + if (a.bar !== b.bar) {
532 + result |= 0b10;
533 + }
534 + return result;
535 + });
536 + const Consumer = getConsumer(Context);
537
424 - ReactNoop.render(<App value={NaN} />);
425 - expect(ReactNoop.flush()).toEqual([
426 - 'App',
427 - 'Provider',
428 - 'Indirection',
429 - 'Indirection',
430 - 'Consumer',
431 - 'Consumer render prop',
432 - ]);
433 - expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
434 -
435 - // Update
436 - ReactNoop.render(<App value={NaN} />);
437 - expect(ReactNoop.flush()).toEqual([
438 - 'App',
439 - 'Provider',
440 - // Consumer should not re-render again
441 - // 'Consumer render prop',
442 - ]);
443 - expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
444 - });
538 + function Provider(props) {
539 + return (
540 + <Context.Provider value={{foo: props.foo, bar: props.bar}}>
541 + {props.children}
542 + </Context.Provider>
543 + );
544 + }
545
446 - it('context unwinds when interrupted', () => {
447 - const Context = React.createContext('Default');
546 + function Foo() {
547 + return (
548 + <Consumer unstable_observedBits={0b01}>
549 + {value => {
550 + ReactNoop.yield('Foo');
551 + return <span prop={'Foo: ' + value.foo} />;
552 + }}
553 + </Consumer>
554 + );
555 + }
556
449 - function Consumer(props) {
450 - return (
451 - <Context.Consumer>
452 - {value => <span prop={'Result: ' + value} />}
453 - </Context.Consumer>
454 - );
455 - }
557 + function Bar() {
558 + return (
559 + <Consumer unstable_observedBits={0b10}>
560 + {value => {
561 + ReactNoop.yield('Bar');
562 + return <span prop={'Bar: ' + value.bar} />;
563 + }}
564 + </Consumer>
565 + );
566 + }
567
457 - function BadRender() {
458 - throw new Error('Bad render');
459 - }
568 + class Indirection extends React.Component {
569 + shouldComponentUpdate() {
570 + return false;
571 + }
572 + render() {
573 + return this.props.children;
574 + }
575 + }
576
461 - class ErrorBoundary extends React.Component {
462 - state = {error: null};
463 - componentDidCatch(error) {
464 - this.setState({error});
465 - }
466 - render() {
467 - if (this.state.error) {
468 - return null;
577 + function App(props) {
578 + return (
579 + <Provider foo={props.foo} bar={props.bar}>
580 + <Indirection>
581 + <Indirection>
582 + <Foo />
583 + </Indirection>
584 + <Indirection>
585 + <Bar />
586 + </Indirection>
587 + </Indirection>
588 + </Provider>
589 + );
590 }
470 - return this.props.children;
471 - }
472 - }
591
474 - function App(props) {
475 - return (
476 - <React.Fragment>
477 - <Context.Provider value="Does not unwind">
478 - <ErrorBoundary>
479 - <Context.Provider value="Unwinds after BadRender throws">
480 - <BadRender />
481 - </Context.Provider>
482 - </ErrorBoundary>
483 - <Consumer />
484 - </Context.Provider>
485 - </React.Fragment>
486 - );
487 - }
592 + ReactNoop.render(<App foo={1} bar={1} />);
593 + expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
594 + expect(ReactNoop.getChildren()).toEqual([
595 + span('Foo: 1'),
596 + span('Bar: 1'),
597 + ]);
598 +
599 + // Update only foo
600 + ReactNoop.render(<App foo={2} bar={1} />);
601 + expect(ReactNoop.flush()).toEqual(['Foo']);
602 + expect(ReactNoop.getChildren()).toEqual([
603 + span('Foo: 2'),
604 + span('Bar: 1'),
605 + ]);
606 +
607 + // Update only bar
608 + ReactNoop.render(<App foo={2} bar={2} />);
609 + expect(ReactNoop.flush()).toEqual(['Bar']);
610 + expect(ReactNoop.getChildren()).toEqual([
611 + span('Foo: 2'),
612 + span('Bar: 2'),
613 + ]);
614 +
615 + // Update both
616 + ReactNoop.render(<App foo={3} bar={3} />);
617 + expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
618 + expect(ReactNoop.getChildren()).toEqual([
619 + span('Foo: 3'),
620 + span('Bar: 3'),
621 + ]);
622 + });
623 +
624 + it('can skip parents with bitmask bailout while updating their children', () => {
625 + const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
626 + let result = 0;
627 + if (a.foo !== b.foo) {
628 + result |= 0b01;
629 + }
630 + if (a.bar !== b.bar) {
631 + result |= 0b10;
632 + }
633 + return result;
634 + });
635 + const Consumer = getConsumer(Context);
636
489 - ReactNoop.render(<App value="A" />);
490 - ReactNoop.flush();
491 - expect(ReactNoop.getChildren()).toEqual([
492 - // The second provider should use the default value.
493 - span('Result: Does not unwind'),
494 - ]);
495 - });
637 + function Provider(props) {
638 + return (
639 + <Context.Provider value={{foo: props.foo, bar: props.bar}}>
640 + {props.children}
641 + </Context.Provider>
642 + );
643 + }
644
497 - it('can skip consumers with bitmask', () => {
498 - const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
499 - let result = 0;
500 - if (a.foo !== b.foo) {
501 - result |= 0b01;
502 - }
503 - if (a.bar !== b.bar) {
504 - result |= 0b10;
505 - }
506 - return result;
507 - });
645 + function Foo(props) {
646 + return (
647 + <Consumer unstable_observedBits={0b01}>
648 + {value => {
649 + ReactNoop.yield('Foo');
650 + return (
651 + <React.Fragment>
652 + <span prop={'Foo: ' + value.foo} />
653 + {props.children && props.children()}
654 + </React.Fragment>
655 + );
656 + }}
657 + </Consumer>
658 + );
659 + }
660
509 - function Provider(props) {
510 - return (
511 - <Context.Provider value={{foo: props.foo, bar: props.bar}}>
512 - {props.children}
513 - </Context.Provider>
514 - );
515 - }
661 + function Bar(props) {
662 + return (
663 + <Consumer unstable_observedBits={0b10}>
664 + {value => {
665 + ReactNoop.yield('Bar');
666 + return (
667 + <React.Fragment>
668 + <span prop={'Bar: ' + value.bar} />
669 + {props.children && props.children()}
670 + </React.Fragment>
671 + );
672 + }}
673 + </Consumer>
674 + );
675 + }
676
517 - function Foo() {
518 - return (
519 - <Context.Consumer unstable_observedBits={0b01}>
520 - {value => {
521 - ReactNoop.yield('Foo');
522 - return <span prop={'Foo: ' + value.foo} />;
523 - }}
524 - </Context.Consumer>
525 - );
526 - }
677 + class Indirection extends React.Component {
678 + shouldComponentUpdate() {
679 + return false;
680 + }
681 + render() {
682 + return this.props.children;
683 + }
684 + }
685
528 - function Bar() {
529 - return (
530 - <Context.Consumer unstable_observedBits={0b10}>
531 - {value => {
532 - ReactNoop.yield('Bar');
533 - return <span prop={'Bar: ' + value.bar} />;
534 - }}
535 - </Context.Consumer>
536 - );
537 - }
686 + function App(props) {
687 + return (
688 + <Provider foo={props.foo} bar={props.bar}>
689 + <Indirection>
690 + <Foo>
691 + {/* Use a render prop so we don't test constant elements. */}
692 + {() => (
693 + <Indirection>
694 + <Bar>
695 + {() => (
696 + <Indirection>
697 + <Foo />
698 + </Indirection>
699 + )}
700 + </Bar>
701 + </Indirection>
702 + )}
703 + </Foo>
704 + </Indirection>
705 + </Provider>
706 + );
707 + }
708
539 - class Indirection extends React.Component {
540 - shouldComponentUpdate() {
541 - return false;
542 - }
543 - render() {
544 - return this.props.children;
545 - }
546 - }
709 + ReactNoop.render(<App foo={1} bar={1} />);
710 + expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
711 + expect(ReactNoop.getChildren()).toEqual([
712 + span('Foo: 1'),
713 + span('Bar: 1'),
714 + span('Foo: 1'),
715 + ]);
716 +
717 + // Update only foo
718 + ReactNoop.render(<App foo={2} bar={1} />);
719 + expect(ReactNoop.flush()).toEqual(['Foo', 'Foo']);
720 + expect(ReactNoop.getChildren()).toEqual([
721 + span('Foo: 2'),
722 + span('Bar: 1'),
723 + span('Foo: 2'),
724 + ]);
725 +
726 + // Update only bar
727 + ReactNoop.render(<App foo={2} bar={2} />);
728 + expect(ReactNoop.flush()).toEqual(['Bar']);
729 + expect(ReactNoop.getChildren()).toEqual([
730 + span('Foo: 2'),
731 + span('Bar: 2'),
732 + span('Foo: 2'),
733 + ]);
734 +
735 + // Update both
736 + ReactNoop.render(<App foo={3} bar={3} />);
737 + expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
738 + expect(ReactNoop.getChildren()).toEqual([
739 + span('Foo: 3'),
740 + span('Bar: 3'),
741 + span('Foo: 3'),
742 + ]);
743 + });
744 +
745 + it("does not re-render if there's an update in a child", () => {
746 + const Context = React.createContext(0);
747 + const Consumer = getConsumer(Context);
748 +
749 + let child;
750 + class Child extends React.Component {
751 + state = {step: 0};
752 + render() {
753 + ReactNoop.yield('Child');
754 + return (
755 + <span
756 + prop={`Context: ${this.props.context}, Step: ${
757 + this.state.step
758 + }`}
759 + />
760 + );
761 + }
762 + }
763
548 - function App(props) {
549 - return (
550 - <Provider foo={props.foo} bar={props.bar}>
551 - <Indirection>
552 - <Indirection>
553 - <Foo />
554 - </Indirection>
555 - <Indirection>
556 - <Bar />
557 - </Indirection>
558 - </Indirection>
559 - </Provider>
560 - );
561 - }
764 + function App(props) {
765 + return (
766 + <Context.Provider value={props.value}>
767 + <Consumer>
768 + {value => {
769 + ReactNoop.yield('Consumer render prop');
770 + return <Child ref={inst => (child = inst)} context={value} />;
771 + }}
772 + </Consumer>
773 + </Context.Provider>
774 + );
775 + }
776
563 - ReactNoop.render(<App foo={1} bar={1} />);
564 - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
565 - expect(ReactNoop.getChildren()).toEqual([span('Foo: 1'), span('Bar: 1')]);
777 + // Initial mount
778 + ReactNoop.render(<App value={1} />);
779 + expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']);
780 + expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]);
781
567 - // Update only foo
568 - ReactNoop.render(<App foo={2} bar={1} />);
569 - expect(ReactNoop.flush()).toEqual(['Foo']);
570 - expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 1')]);
782 + child.setState({step: 1});
783 + expect(ReactNoop.flush()).toEqual(['Child']);
784 + expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]);
785 + });
786
572 - // Update only bar
573 - ReactNoop.render(<App foo={2} bar={2} />);
574 - expect(ReactNoop.flush()).toEqual(['Bar']);
575 - expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 2')]);
787 + it('consumer bails out if value is unchanged and something above bailed out', () => {
788 + const Context = React.createContext(0);
789 + const Consumer = getConsumer(Context);
790
577 - // Update both
578 - ReactNoop.render(<App foo={3} bar={3} />);
579 - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
580 - expect(ReactNoop.getChildren()).toEqual([span('Foo: 3'), span('Bar: 3')]);
581 - });
791 + function renderChildValue(value) {
792 + ReactNoop.yield('Consumer');
793 + return <span prop={value} />;
794 + }
795
583 - it('can skip parents with bitmask bailout while updating their children', () => {
584 - const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
585 - let result = 0;
586 - if (a.foo !== b.foo) {
587 - result |= 0b01;
588 - }
589 - if (a.bar !== b.bar) {
590 - result |= 0b10;
591 - }
592 - return result;
593 - });
796 + function ChildWithInlineRenderCallback() {
797 + ReactNoop.yield('ChildWithInlineRenderCallback');
798 + // Note: we are intentionally passing an inline arrow. Don't refactor.
799 + return <Consumer>{value => renderChildValue(value)}</Consumer>;
800 + }
801
595 - function Provider(props) {
596 - return (
597 - <Context.Provider value={{foo: props.foo, bar: props.bar}}>
598 - {props.children}
599 - </Context.Provider>
600 - );
601 - }
802 + function ChildWithCachedRenderCallback() {
803 + ReactNoop.yield('ChildWithCachedRenderCallback');
804 + return <Consumer>{renderChildValue}</Consumer>;
805 + }
806
603 - function Foo(props) {
604 - return (
605 - <Context.Consumer unstable_observedBits={0b01}>
606 - {value => {
607 - ReactNoop.yield('Foo');
807 + class PureIndirection extends React.PureComponent {
808 + render() {
809 + ReactNoop.yield('PureIndirection');
810 return (
811 <React.Fragment>
610 - <span prop={'Foo: ' + value.foo} />
611 - {props.children && props.children()}
812 + <ChildWithInlineRenderCallback />
813 + <ChildWithCachedRenderCallback />
814 </React.Fragment>
815 );
614 - }}
615 - </Context.Consumer>
616 - );
617 - }
816 + }
817 + }
818
619 - function Bar(props) {
620 - return (
621 - <Context.Consumer unstable_observedBits={0b10}>
622 - {value => {
623 - ReactNoop.yield('Bar');
819 + class App extends React.Component {
820 + render() {
821 + ReactNoop.yield('App');
822 return (
625 - <React.Fragment>
626 - <span prop={'Bar: ' + value.bar} />
627 - {props.children && props.children()}
628 - </React.Fragment>
823 + <Context.Provider value={this.props.value}>
824 + <PureIndirection />
825 + </Context.Provider>
826 );
630 - }}
631 - </Context.Consumer>
632 - );
633 - }
827 + }
828 + }
829
635 - class Indirection extends React.Component {
636 - shouldComponentUpdate() {
637 - return false;
638 - }
639 - render() {
640 - return this.props.children;
641 - }
642 - }
830 + // Initial mount
831 + ReactNoop.render(<App value={1} />);
832 + expect(ReactNoop.flush()).toEqual([
833 + 'App',
834 + 'PureIndirection',
835 + 'ChildWithInlineRenderCallback',
836 + 'Consumer',
837 + 'ChildWithCachedRenderCallback',
838 + 'Consumer',
839 + ]);
840 + expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
841 +
842 + // Update (bailout)
843 + ReactNoop.render(<App value={1} />);
844 + expect(ReactNoop.flush()).toEqual(['App']);
845 + expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
846 +
847 + // Update (no bailout)
848 + ReactNoop.render(<App value={2} />);
849 + expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']);
850 + expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
851 + });
852 +
853 + // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
854 + // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
855 + // If we bailed out on referential equality, it would be confusing that you
856 + // can call this.setState(), but an autobound render callback "blocked" the update.
857 + // https://github.com/facebook/react/pull/12470#issuecomment-376917711
858 + it('consumer does not bail out if there were no bailouts above it', () => {
859 + const Context = React.createContext(0);
860 + const Consumer = getConsumer(Context);
861 +
862 + class App extends React.Component {
863 + state = {
864 + text: 'hello',
865 + };
866 +
867 + renderConsumer = context => {
868 + ReactNoop.yield('App#renderConsumer');
869 + return <span prop={this.state.text} />;
870 + };
871 +
872 + render() {
873 + ReactNoop.yield('App');
874 + return (
875 + <Context.Provider value={this.props.value}>
876 + <Consumer>{this.renderConsumer}</Consumer>
877 + </Context.Provider>
878 + );
879 + }
880 + }
881
644 - function App(props) {
645 - return (
646 - <Provider foo={props.foo} bar={props.bar}>
647 - <Indirection>
648 - <Foo>
649 - {/* Use a render prop so we don't test constant elements. */}
650 - {() => (
651 - <Indirection>
652 - <Bar>
653 - {() => (
654 - <Indirection>
655 - <Foo />
656 - </Indirection>
657 - )}
658 - </Bar>
659 - </Indirection>
660 - )}
661 - </Foo>
662 - </Indirection>
663 - </Provider>
664 - );
665 - }
882 + // Initial mount
883 + let inst;
884 + ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
885 + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
886 + expect(ReactNoop.getChildren()).toEqual([span('hello')]);
887 +
888 + // Update
889 + inst.setState({text: 'goodbye'});
890 + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
891 + expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
892 + });
893 +
894 + // This is a regression case for https://github.com/facebook/react/issues/12389.
895 + it('does not run into an infinite loop', () => {
896 + const Context = React.createContext(null);
897 + const Consumer = getConsumer(Context);
898 +
899 + class App extends React.Component {
900 + renderItem(id) {
901 + return (
902 + <span key={id}>
903 + <Consumer>{() => <span>inner</span>}</Consumer>
904 + <span>outer</span>
905 + </span>
906 + );
907 + }
908 + renderList() {
909 + const list = [1, 2].map(id => this.renderItem(id));
910 + if (this.props.reverse) {
911 + list.reverse();
912 + }
913 + return list;
914 + }
915 + render() {
916 + return (
917 + <Context.Provider value={{}}>
918 + {this.renderList()}
919 + </Context.Provider>
920 + );
921 + }
922 + }
923
667 - ReactNoop.render(<App foo={1} bar={1} />);
668 - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
669 - expect(ReactNoop.getChildren()).toEqual([
670 - span('Foo: 1'),
671 - span('Bar: 1'),
672 - span('Foo: 1'),
673 - ]);
674 -
675 - // Update only foo
676 - ReactNoop.render(<App foo={2} bar={1} />);
677 - expect(ReactNoop.flush()).toEqual(['Foo', 'Foo']);
678 - expect(ReactNoop.getChildren()).toEqual([
679 - span('Foo: 2'),
680 - span('Bar: 1'),
681 - span('Foo: 2'),
682 - ]);
683 -
684 - // Update only bar
685 - ReactNoop.render(<App foo={2} bar={2} />);
686 - expect(ReactNoop.flush()).toEqual(['Bar']);
687 - expect(ReactNoop.getChildren()).toEqual([
688 - span('Foo: 2'),
689 - span('Bar: 2'),
690 - span('Foo: 2'),
691 - ]);
692 -
693 - // Update both
694 - ReactNoop.render(<App foo={3} bar={3} />);
695 - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
696 - expect(ReactNoop.getChildren()).toEqual([
697 - span('Foo: 3'),
698 - span('Bar: 3'),
699 - span('Foo: 3'),
700 - ]);
701 - });
924 + ReactNoop.render(<App reverse={false} />);
925 + ReactNoop.flush();
926 + ReactNoop.render(<App reverse={true} />);
927 + ReactNoop.flush();
928 + ReactNoop.render(<App reverse={false} />);
929 + ReactNoop.flush();
930 + });
931
703 - it('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
704 - const Context = React.createContext(
705 - 0,
706 - (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
707 - );
932 + // This is a regression case for https://github.com/facebook/react/issues/12686
933 + it('does not skip some siblings', () => {
934 + const Context = React.createContext(0);
935 + const ContextConsumer = getConsumer(Context);
936
709 - function App(props) {
710 - return <Context.Provider value={props.value} />;
711 - }
937 + class App extends React.Component {
938 + state = {
939 + step: 0,
940 + };
941
713 - ReactNoop.render(<App value={1} />);
714 - ReactNoop.flush();
942 + render() {
943 + ReactNoop.yield('App');
944 + return (
945 + <Context.Provider value={this.state.step}>
946 + <StaticContent />
947 + {this.state.step > 0 && <Indirection />}
948 + </Context.Provider>
949 + );
950 + }
951 + }
952
716 - // Update
717 - ReactNoop.render(<App value={2} />);
718 - expect(ReactNoop.flush).toWarnDev(
719 - 'calculateChangedBits: Expected the return value to be a 31-bit ' +
720 - 'integer. Instead received: 4294967295',
721 - );
722 - });
953 + class StaticContent extends React.PureComponent {
954 + render() {
955 + return (
956 + <React.Fragment>
957 + <React.Fragment>
958 + <span prop="static 1" />
959 + <span prop="static 2" />
960 + </React.Fragment>
961 + </React.Fragment>
962 + );
963 + }
964 + }
965
724 - it('warns if multiple renderers concurrently render the same context', () => {
725 - spyOnDev(console, 'error');
726 - const Context = React.createContext(0);
966 + class Indirection extends React.PureComponent {
967 + render() {
968 + return (
969 + <ContextConsumer>
970 + {value => {
971 + ReactNoop.yield('Consumer');
972 + return <span prop={value} />;
973 + }}
974 + </ContextConsumer>
975 + );
976 + }
977 + }
978
728 - function Foo(props) {
729 - ReactNoop.yield('Foo');
730 - return null;
731 - }
979 + // Initial mount
980 + let inst;
981 + ReactNoop.render(<App ref={ref => (inst = ref)} />);
982 + expect(ReactNoop.flush()).toEqual(['App']);
983 + expect(ReactNoop.getChildren()).toEqual([
984 + span('static 1'),
985 + span('static 2'),
986 + ]);
987 + // Update the first time
988 + inst.setState({step: 1});
989 + expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
990 + expect(ReactNoop.getChildren()).toEqual([
991 + span('static 1'),
992 + span('static 2'),
993 + span(1),
994 + ]);
995 + // Update the second time
996 + inst.setState({step: 2});
997 + expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
998 + expect(ReactNoop.getChildren()).toEqual([
999 + span('static 1'),
1000 + span('static 2'),
1001 + span(2),
1002 + ]);
1003 + });
1004 + });
1005 + }
1006
733 - function App(props) {
734 - return (
735 - <Context.Provider value={props.value}>
736 - <Foo />
737 - <Foo />
738 - </Context.Provider>
1007 + describe('Context.Provider', () => {
1008 + it('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
1009 + const Context = React.createContext(
1010 + 0,
1011 + (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
1012 );
740 - }
1013
742 - ReactNoop.render(<App value={1} />);
743 - // Render past the Provider, but don't commit yet
744 - ReactNoop.flushThrough(['Foo']);
745 -
746 - // Get a new copy of ReactNoop
747 - jest.resetModules();
748 - ReactFeatureFlags = require('shared/ReactFeatureFlags');
749 - React = require('react');
750 - ReactNoop = require('react-noop-renderer');
1014 + function App(props) {
1015 + return <Context.Provider value={props.value} />;
1016 + }
1017
752 - // Render the provider again using a different renderer
753 - ReactNoop.render(<App value={1} />);
754 - ReactNoop.flush();
1018 + ReactNoop.render(<App value={1} />);
1019 + ReactNoop.flush();
1020
756 - if (__DEV__) {
757 - expect(console.error.calls.argsFor(0)[0]).toContain(
758 - 'Detected multiple renderers concurrently rendering the same ' +
759 - 'context provider. This is currently unsupported',
1021 + // Update
1022 + ReactNoop.render(<App value={2} />);
1023 + expect(ReactNoop.flush).toWarnDev(
1024 + 'calculateChangedBits: Expected the return value to be a 31-bit ' +
1025 + 'integer. Instead received: 4294967295',
1026 );
761 - }
762 - });
1027 + });
1028
764 - it('warns if consumer child is not a function', () => {
765 - spyOnDev(console, 'error');
766 - const Context = React.createContext(0);
767 - ReactNoop.render(<Context.Consumer />);
768 - expect(ReactNoop.flush).toThrow('render is not a function');
769 - if (__DEV__) {
770 - expect(console.error.calls.argsFor(0)[0]).toContain(
771 - 'A context consumer was rendered with multiple children, or a child ' +
772 - "that isn't a function",
773 - );
774 - }
775 - });
1029 + it('warns if multiple renderers concurrently render the same context', () => {
1030 + spyOnDev(console, 'error');
1031 + const Context = React.createContext(0);
1032
777 - it("does not re-render if there's an update in a child", () => {
778 - const Context = React.createContext(0);
1033 + function Foo(props) {
1034 + ReactNoop.yield('Foo');
1035 + return null;
1036 + }
1037
780 - let child;
781 - class Child extends React.Component {
782 - state = {step: 0};
783 - render() {
784 - ReactNoop.yield('Child');
1038 + function App(props) {
1039 return (
786 - <span
787 - prop={`Context: ${this.props.context}, Step: ${this.state.step}`}
788 - />
1040 + <Context.Provider value={props.value}>
1041 + <Foo />
1042 + <Foo />
1043 + </Context.Provider>
1044 );
1045 }
791 - }
792 -
793 - function App(props) {
794 - return (
795 - <Context.Provider value={props.value}>
796 - <Context.Consumer>
797 - {value => {
798 - ReactNoop.yield('Consumer render prop');
799 - return <Child ref={inst => (child = inst)} context={value} />;
800 - }}
801 - </Context.Consumer>
802 - </Context.Provider>
803 - );
804 - }
805 -
806 - // Initial mount
807 - ReactNoop.render(<App value={1} />);
808 - expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']);
809 - expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]);
810 -
811 - child.setState({step: 1});
812 - expect(ReactNoop.flush()).toEqual(['Child']);
813 - expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]);
814 - });
815 -
816 - it('provider bails out if children and value are unchanged (like sCU)', () => {
817 - const Context = React.createContext(0);
1046
819 - function Child() {
820 - ReactNoop.yield('Child');
821 - return <span prop="Child" />;
822 - }
823 -
824 - const children = <Child />;
825 -
826 - function App(props) {
827 - ReactNoop.yield('App');
828 - return (
829 - <Context.Provider value={props.value}>{children}</Context.Provider>
830 - );
831 - }
1047 + ReactNoop.render(<App value={1} />);
1048 + // Render past the Provider, but don't commit yet
1049 + ReactNoop.flushThrough(['Foo']);
1050
833 - // Initial mount
834 - ReactNoop.render(<App value={1} />);
835 - expect(ReactNoop.flush()).toEqual(['App', 'Child']);
836 - expect(ReactNoop.getChildren()).toEqual([span('Child')]);
837 -
838 - // Update
839 - ReactNoop.render(<App value={1} />);
840 - expect(ReactNoop.flush()).toEqual([
841 - 'App',
842 - // Child does not re-render
843 - ]);
844 - expect(ReactNoop.getChildren()).toEqual([span('Child')]);
845 - });
1051 + // Get a new copy of ReactNoop
1052 + jest.resetModules();
1053 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
1054 + React = require('react');
1055 + ReactNoop = require('react-noop-renderer');
1056
847 - it('provider does not bail out if legacy context changed above', () => {
848 - const Context = React.createContext(0);
1057 + // Render the provider again using a different renderer
1058 + ReactNoop.render(<App value={1} />);
1059 + ReactNoop.flush();
1060
850 - function Child() {
851 - ReactNoop.yield('Child');
852 - return <span prop="Child" />;
853 - }
1061 + if (__DEV__) {
1062 + expect(console.error.calls.argsFor(0)[0]).toContain(
1063 + 'Detected multiple renderers concurrently rendering the same ' +
1064 + 'context provider. This is currently unsupported',
1065 + );
1066 + }
1067 + });
1068
855 - const children = <Child />;
1069 + it('provider bails out if children and value are unchanged (like sCU)', () => {
1070 + const Context = React.createContext(0);
1071
857 - class LegacyProvider extends React.Component {
858 - static childContextTypes = {
859 - legacyValue: () => {},
860 - };
861 - state = {legacyValue: 1};
862 - getChildContext() {
863 - return {legacyValue: this.state.legacyValue};
864 - }
865 - render() {
866 - ReactNoop.yield('LegacyProvider');
867 - return this.props.children;
1072 + function Child() {
1073 + ReactNoop.yield('Child');
1074 + return <span prop="Child" />;
1075 }
869 - }
1076
871 - class App extends React.Component {
872 - state = {value: 1};
873 - render() {
1077 + const children = <Child />;
1078 +
1079 + function App(props) {
1080 ReactNoop.yield('App');
1081 return (
876 - <Context.Provider value={this.state.value}>
877 - {this.props.children}
878 - </Context.Provider>
1082 + <Context.Provider value={props.value}>{children}</Context.Provider>
1083 );
1084 }
881 - }
882 -
883 - const legacyProviderRef = React.createRef();
884 - const appRef = React.createRef();
885 -
886 - // Initial mount
887 - ReactNoop.render(
888 - <LegacyProvider ref={legacyProviderRef}>
889 - <App ref={appRef} value={1}>
890 - {children}
891 - </App>
892 - </LegacyProvider>,
893 - );
894 - expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
895 - expect(ReactNoop.getChildren()).toEqual([span('Child')]);
896 -
897 - // Update App with same value (should bail out)
898 - appRef.current.setState({value: 1});
899 - expect(ReactNoop.flush()).toEqual(['App']);
900 - expect(ReactNoop.getChildren()).toEqual([span('Child')]);
901 -
902 - // Update LegacyProvider (should not bail out)
903 - legacyProviderRef.current.setState({value: 1});
904 - expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
905 - expect(ReactNoop.getChildren()).toEqual([span('Child')]);
906 -
907 - // Update App with same value (should bail out)
908 - appRef.current.setState({value: 1});
909 - expect(ReactNoop.flush()).toEqual(['App']);
910 - expect(ReactNoop.getChildren()).toEqual([span('Child')]);
911 - });
1085
913 - it('consumer bails out if value is unchanged and something above bailed out', () => {
914 - const Context = React.createContext(0);
1086 + // Initial mount
1087 + ReactNoop.render(<App value={1} />);
1088 + expect(ReactNoop.flush()).toEqual(['App', 'Child']);
1089 + expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1090 +
1091 + // Update
1092 + ReactNoop.render(<App value={1} />);
1093 + expect(ReactNoop.flush()).toEqual([
1094 + 'App',
1095 + // Child does not re-render
1096 + ]);
1097 + expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1098 + });
1099
916 - function renderChildValue(value) {
917 - ReactNoop.yield('Consumer');
918 - return <span prop={value} />;
919 - }
1100 + it('provider does not bail out if legacy context changed above', () => {
1101 + const Context = React.createContext(0);
1102
921 - function ChildWithInlineRenderCallback() {
922 - ReactNoop.yield('ChildWithInlineRenderCallback');
923 - // Note: we are intentionally passing an inline arrow. Don't refactor.
924 - return (
925 - <Context.Consumer>{value => renderChildValue(value)}</Context.Consumer>
926 - );
927 - }
1103 + function Child() {
1104 + ReactNoop.yield('Child');
1105 + return <span prop="Child" />;
1106 + }
1107
929 - function ChildWithCachedRenderCallback() {
930 - ReactNoop.yield('ChildWithCachedRenderCallback');
931 - return <Context.Consumer>{renderChildValue}</Context.Consumer>;
932 - }
1108 + const children = <Child />;
1109
934 - class PureIndirection extends React.PureComponent {
935 - render() {
936 - ReactNoop.yield('PureIndirection');
937 - return (
938 - <React.Fragment>
939 - <ChildWithInlineRenderCallback />
940 - <ChildWithCachedRenderCallback />
941 - </React.Fragment>
942 - );
1110 + class LegacyProvider extends React.Component {
1111 + static childContextTypes = {
1112 + legacyValue: () => {},
1113 + };
1114 + state = {legacyValue: 1};
1115 + getChildContext() {
1116 + return {legacyValue: this.state.legacyValue};
1117 + }
1118 + render() {
1119 + ReactNoop.yield('LegacyProvider');
1120 + return this.props.children;
1121 + }
1122 }
944 - }
1123
946 - class App extends React.Component {
947 - render() {
948 - ReactNoop.yield('App');
949 - return (
950 - <Context.Provider value={this.props.value}>
951 - <PureIndirection />
952 - </Context.Provider>
953 - );
1124 + class App extends React.Component {
1125 + state = {value: 1};
1126 + render() {
1127 + ReactNoop.yield('App');
1128 + return (
1129 + <Context.Provider value={this.state.value}>
1130 + {this.props.children}
1131 + </Context.Provider>
1132 + );
1133 + }
1134 }
955 - }
1135
957 - // Initial mount
958 - ReactNoop.render(<App value={1} />);
959 - expect(ReactNoop.flush()).toEqual([
960 - 'App',
961 - 'PureIndirection',
962 - 'ChildWithInlineRenderCallback',
963 - 'Consumer',
964 - 'ChildWithCachedRenderCallback',
965 - 'Consumer',
966 - ]);
967 - expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
968 -
969 - // Update (bailout)
970 - ReactNoop.render(<App value={1} />);
971 - expect(ReactNoop.flush()).toEqual(['App']);
972 - expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
973 -
974 - // Update (no bailout)
975 - ReactNoop.render(<App value={2} />);
976 - expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']);
977 - expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
978 - });
979 -
980 - // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
981 - // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
982 - // If we bailed out on referential equality, it would be confusing that you
983 - // can call this.setState(), but an autobound render callback "blocked" the update.
984 - // https://github.com/facebook/react/pull/12470#issuecomment-376917711
985 - it('consumer does not bail out if there were no bailouts above it', () => {
986 - const Context = React.createContext(0);
1136 + const legacyProviderRef = React.createRef();
1137 + const appRef = React.createRef();
1138
988 - class App extends React.Component {
989 - state = {
990 - text: 'hello',
991 - };
992 -
993 - renderConsumer = context => {
994 - ReactNoop.yield('App#renderConsumer');
995 - return <span prop={this.state.text} />;
996 - };
1139 + // Initial mount
1140 + ReactNoop.render(
1141 + <LegacyProvider ref={legacyProviderRef}>
1142 + <App ref={appRef} value={1}>
1143 + {children}
1144 + </App>
1145 + </LegacyProvider>,
1146 + );
1147 + expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
1148 + expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1149 +
1150 + // Update App with same value (should bail out)
1151 + appRef.current.setState({value: 1});
1152 + expect(ReactNoop.flush()).toEqual(['App']);
1153 + expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1154 +
1155 + // Update LegacyProvider (should not bail out)
1156 + legacyProviderRef.current.setState({value: 1});
1157 + expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
1158 + expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1159 +
1160 + // Update App with same value (should bail out)
1161 + appRef.current.setState({value: 1});
1162 + expect(ReactNoop.flush()).toEqual(['App']);
1163 + expect(ReactNoop.getChildren()).toEqual([span('Child')]);
1164 + });
1165 + });
1166
998 - render() {
999 - ReactNoop.yield('App');
1000 - return (
1001 - <Context.Provider value={this.props.value}>
1002 - <Context.Consumer>{this.renderConsumer}</Context.Consumer>
1003 - </Context.Provider>
1167 + describe('Context.Consumer', () => {
1168 + it('warns if child is not a function', () => {
1169 + spyOnDev(console, 'error');
1170 + const Context = React.createContext(0);
1171 + ReactNoop.render(<Context.Consumer />);
1172 + expect(ReactNoop.flush).toThrow('render is not a function');
1173 + if (__DEV__) {
1174 + expect(console.error.calls.argsFor(0)[0]).toContain(
1175 + 'A context consumer was rendered with multiple children, or a child ' +
1176 + "that isn't a function",
1177 );
1178 }
1006 - }
1007 -
1008 - // Initial mount
1009 - let inst;
1010 - ReactNoop.render(<App value={1} ref={ref => (inst = ref)} />);
1011 - expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1012 - expect(ReactNoop.getChildren()).toEqual([span('hello')]);
1013 -
1014 - // Update
1015 - inst.setState({text: 'goodbye'});
1016 - expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
1017 - expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1018 - });
1179 + });
1180
1020 - // This is a regression case for https://github.com/facebook/react/issues/12389.
1021 - it('does not run into an infinite loop', () => {
1022 - const Context = React.createContext(null);
1181 + it('can read other contexts inside consumer render prop', () => {
1182 + const FooContext = React.createContext(0);
1183 + const BarContext = React.createContext(0);
1184
1024 - class App extends React.Component {
1025 - renderItem(id) {
1185 + function FooAndBar() {
1186 return (
1027 - <span key={id}>
1028 - <Context.Consumer>{() => <span>inner</span>}</Context.Consumer>
1029 - <span>outer</span>
1030 - </span>
1187 + <FooContext>
1188 + {foo => {
1189 + const bar = BarContext.unstable_read();
1190 + return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
1191 + }}
1192 + </FooContext>
1193 );
1194 }
1033 - renderList() {
1034 - const list = [1, 2].map(id => this.renderItem(id));
1035 - if (this.props.reverse) {
1036 - list.reverse();
1195 +
1196 + class Indirection extends React.Component {
1197 + shouldComponentUpdate() {
1198 + return false;
1199 + }
1200 + render() {
1201 + return this.props.children;
1202 }
1038 - return list;
1203 }
1040 - render() {
1204 +
1205 + function App(props) {
1206 return (
1042 - <Context.Provider value={{}}>{this.renderList()}</Context.Provider>
1207 + <FooContext.Provider value={props.foo}>
1208 + <BarContext.Provider value={props.bar}>
1209 + <Indirection>
1210 + <FooAndBar />
1211 + </Indirection>
1212 + </BarContext.Provider>
1213 + </FooContext.Provider>
1214 );
1215 }
1045 - }
1216
1047 - ReactNoop.render(<App reverse={false} />);
1048 - ReactNoop.flush();
1049 - ReactNoop.render(<App reverse={true} />);
1050 - ReactNoop.flush();
1051 - ReactNoop.render(<App reverse={false} />);
1052 - ReactNoop.flush();
1053 - });
1217 + ReactNoop.render(<App foo={1} bar={1} />);
1218 + expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1']);
1219 + expect(ReactNoop.getChildren()).toEqual([span('Foo: 1, Bar: 1')]);
1220
1055 - // This is a regression case for https://github.com/facebook/react/issues/12686
1056 - it('does not skip some siblings', () => {
1057 - const Context = React.createContext(0);
1221 + // Update foo
1222 + ReactNoop.render(<App foo={2} bar={1} />);
1223 + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']);
1224 + expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 1')]);
1225
1059 - class App extends React.Component {
1060 - state = {
1061 - step: 0,
1062 - };
1226 + // Update bar
1227 + ReactNoop.render(<App foo={2} bar={2} />);
1228 + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
1229 + expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 2')]);
1230 + });
1231 + });
1232
1064 - render() {
1065 - ReactNoop.yield('App');
1233 + describe('unstable_readContext', () => {
1234 + it('can use the same context multiple times in the same function', () => {
1235 + const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
1236 + let result = 0;
1237 + if (a.foo !== b.foo) {
1238 + result |= 0b001;
1239 + }
1240 + if (a.bar !== b.bar) {
1241 + result |= 0b010;
1242 + }
1243 + if (a.baz !== b.baz) {
1244 + result |= 0b100;
1245 + }
1246 + return result;
1247 + });
1248 +
1249 + function Provider(props) {
1250 return (
1067 - <Context.Provider value={this.state.step}>
1068 - <StaticContent />
1069 - {this.state.step > 0 && <Indirection />}
1251 + <Context.Provider
1252 + value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
1253 + {props.children}
1254 </Context.Provider>
1255 );
1256 }
1073 - }
1257
1075 - class StaticContent extends React.PureComponent {
1076 - render() {
1077 - return (
1078 - <React.Fragment>
1079 - <React.Fragment>
1080 - <span prop="static 1" />
1081 - <span prop="static 2" />
1082 - </React.Fragment>
1083 - </React.Fragment>
1084 - );
1258 + function FooAndBar() {
1259 + const {foo} = Context.unstable_read(0b001);
1260 + const {bar} = Context.unstable_read(0b010);
1261 + return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
1262 }
1086 - }
1263
1088 - class Indirection extends React.PureComponent {
1089 - render() {
1090 - return <Consumer />;
1264 + function Baz() {
1265 + const {baz} = Context.unstable_read(0b100);
1266 + return <Text text={'Baz: ' + baz} />;
1267 }
1092 - }
1268
1094 - function Consumer() {
1095 - return (
1096 - <Context.Consumer>
1097 - {value => {
1098 - ReactNoop.yield('Consumer');
1099 - return <span prop={value} />;
1100 - }}
1101 - </Context.Consumer>
1102 - );
1103 - }
1269 + class Indirection extends React.Component {
1270 + shouldComponentUpdate() {
1271 + return false;
1272 + }
1273 + render() {
1274 + return this.props.children;
1275 + }
1276 + }
1277
1105 - // Initial mount
1106 - let inst;
1107 - ReactNoop.render(<App ref={ref => (inst = ref)} />);
1108 - expect(ReactNoop.flush()).toEqual(['App']);
1109 - expect(ReactNoop.getChildren()).toEqual([
1110 - span('static 1'),
1111 - span('static 2'),
1112 - ]);
1113 - // Update the first time
1114 - inst.setState({step: 1});
1115 - expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
1116 - expect(ReactNoop.getChildren()).toEqual([
1117 - span('static 1'),
1118 - span('static 2'),
1119 - span(1),
1120 - ]);
1121 - // Update the second time
1122 - inst.setState({step: 2});
1123 - expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
1124 - expect(ReactNoop.getChildren()).toEqual([
1125 - span('static 1'),
1126 - span('static 2'),
1127 - span(2),
1128 - ]);
1278 + function App(props) {
1279 + return (
1280 + <Provider foo={props.foo} bar={props.bar} baz={props.baz}>
1281 + <Indirection>
1282 + <Indirection>
1283 + <FooAndBar />
1284 + </Indirection>
1285 + <Indirection>
1286 + <Baz />
1287 + </Indirection>
1288 + </Indirection>
1289 + </Provider>
1290 + );
1291 + }
1292 +
1293 + ReactNoop.render(<App foo={1} bar={1} baz={1} />);
1294 + expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1', 'Baz: 1']);
1295 + expect(ReactNoop.getChildren()).toEqual([
1296 + span('Foo: 1, Bar: 1'),
1297 + span('Baz: 1'),
1298 + ]);
1299 +
1300 + // Update only foo
1301 + ReactNoop.render(<App foo={2} bar={1} baz={1} />);
1302 + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']);
1303 + expect(ReactNoop.getChildren()).toEqual([
1304 + span('Foo: 2, Bar: 1'),
1305 + span('Baz: 1'),
1306 + ]);
1307 +
1308 + // Update only bar
1309 + ReactNoop.render(<App foo={2} bar={2} baz={1} />);
1310 + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
1311 + expect(ReactNoop.getChildren()).toEqual([
1312 + span('Foo: 2, Bar: 2'),
1313 + span('Baz: 1'),
1314 + ]);
1315 +
1316 + // Update only baz
1317 + ReactNoop.render(<App foo={2} bar={2} baz={2} />);
1318 + expect(ReactNoop.flush()).toEqual(['Baz: 2']);
1319 + expect(ReactNoop.getChildren()).toEqual([
1320 + span('Foo: 2, Bar: 2'),
1321 + span('Baz: 2'),
1322 + ]);
1323 + });
1324 });
1325
1326 it('unwinds after errors in complete phase', () => {
packages/react/src/ReactContext.js
+18
@@ -11,8 +11,24 @@ import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
11
12 import type {ReactContext} from 'shared/ReactTypes';
13
14 +import invariant from 'shared/invariant';
15 import warningWithoutStack from 'shared/warningWithoutStack';
16
17 +import ReactCurrentOwner from './ReactCurrentOwner';
18 +
19 +export function readContext<T>(
20 + context: ReactContext<T>,
21 + observedBits: void | number | boolean,
22 +): T {
23 + const dispatcher = ReactCurrentOwner.currentDispatcher;
24 + invariant(
25 + dispatcher !== null,
26 + 'Context.unstable_read(): Context can only be read while React is ' +
27 + 'rendering, e.g. inside the render method or getDerivedStateFromProps.',
28 + );
29 + return dispatcher.readContext(context, observedBits);
30 +}
31 +
32 export function createContext<T>(
33 defaultValue: T,
34 calculateChangedBits: ?(a: T, b: T) => number,
@@ -47,6 +63,7 @@ export function createContext<T>(
63 // These are circular
64 Provider: (null: any),
65 Consumer: (null: any),
66 + unstable_read: (null: any),
67 };
68
69 context.Provider = {
@@ -54,6 +71,7 @@ export function createContext<T>(
71 _context: context,
72 };
73 context.Consumer = context;
74 + context.unstable_read = readContext.bind(null, context);
75
76 if (__DEV__) {
77 context._currentRenderer = null;
packages/react/src/ReactCurrentOwner.js
+2
@@ -8,6 +8,7 @@
8 */
9
10 import type {Fiber} from 'react-reconciler/src/ReactFiber';
11 +import typeof {Dispatcher} from 'react-reconciler/src/ReactFiberDispatcher';
12
13 /**
14 * Keeps track of the current owner.
@@ -21,6 +22,7 @@ const ReactCurrentOwner = {
22 * @type {ReactComponent}
23 */
24 current: (null: null | Fiber),
25 + currentDispatcher: (null: null | Dispatcher),
26 };
27
28 export default ReactCurrentOwner;
packages/shared/ReactTypes.js
+1
@@ -79,6 +79,7 @@ export type ReactContext<T> = {
79 $$typeof: Symbol | number,
80 Consumer: ReactContext<T>,
81 Provider: ReactProviderType<T>,
82 + unstable_read: () => T,
83
84 _calculateChangedBits: ((a: T, b: T) => number) | null,
85 _defaultValue: T,