From 8fb71d7f4b9f28e881001cf2fe6a04540b3f95c1 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 2 Jul 2018 13:19:58 -0700 Subject: [PATCH 1/5] 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. --- packages/react-reconciler/src/ReactFiber.js | 7 + .../src/ReactFiberBeginWork.js | 142 ++---------- .../src/ReactFiberNewContext.js | 219 ++++++++++++++++-- 3 files changed, 222 insertions(+), 146 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index e0b325f67ee06..06f4f7a4c81e8 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -14,6 +14,7 @@ import type {TypeOfMode} from './ReactTypeOfMode'; import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {UpdateQueue} from './ReactUpdateQueue'; +import type {ContextDependency} from './ReactFiberNewContext'; import invariant from 'shared/invariant'; import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; @@ -124,6 +125,9 @@ export type Fiber = {| // The state used to create the output memoizedState: any, + // A linked-list of contexts that this fiber depends on + firstContextDependency: ContextDependency | null, + // Bitfield that describes properties about the fiber and its subtree. E.g. // the AsyncMode flag indicates whether the subtree should be async-by- // default. When a fiber is created, it inherits the mode of its @@ -213,6 +217,7 @@ function FiberNode( this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; + this.firstContextDependency = null; this.mode = mode; @@ -331,6 +336,7 @@ export function createWorkInProgress( workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; + workInProgress.firstContextDependency = current.firstContextDependency; // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; @@ -559,6 +565,7 @@ export function assignFiberPropertiesInDEV( target.memoizedProps = source.memoizedProps; target.updateQueue = source.updateQueue; target.memoizedState = source.memoizedState; + target.firstContextDependency = source.firstContextDependency; target.mode = source.mode; target.effectTag = source.effectTag; target.nextEffect = source.nextEffect; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 8eb9e2188b749..e6386ba1d2485 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -70,8 +70,9 @@ import { import {pushHostContext, pushHostContainer} from './ReactFiberHostContext'; import { pushProvider, - getContextCurrentValue, - getContextChangedBits, + propagateContextChange, + readContext, + prepareToReadContext, } from './ReactFiberNewContext'; import { markActualRenderTimeStarted, @@ -809,102 +810,6 @@ function updatePortalComponent(current, workInProgress, renderExpirationTime) { return workInProgress.child; } -function propagateContextChange( - workInProgress: Fiber, - context: ReactContext, - changedBits: number, - renderExpirationTime: ExpirationTime, -): void { - let fiber = workInProgress.child; - if (fiber !== null) { - // Set the return pointer of the child to the work-in-progress fiber. - fiber.return = workInProgress; - } - while (fiber !== null) { - let nextFiber; - // Visit this fiber. - switch (fiber.tag) { - case ContextConsumer: - // Check if the context matches. - const observedBits: number = fiber.stateNode | 0; - if (fiber.type === context && (observedBits & changedBits) !== 0) { - // Update the expiration time of all the ancestors, including - // the alternates. - - let node = fiber; - do { - const alternate = node.alternate; - if ( - node.expirationTime === NoWork || - node.expirationTime > renderExpirationTime - ) { - node.expirationTime = renderExpirationTime; - if ( - alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > renderExpirationTime) - ) { - alternate.expirationTime = renderExpirationTime; - } - } else if ( - alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > renderExpirationTime) - ) { - alternate.expirationTime = renderExpirationTime; - } else { - // Neither alternate was updated, which means the rest of the - // ancestor path already has sufficient priority. - break; - } - node = node.return; - } while (node !== null); - - // Don't scan deeper than a matching consumer. When we render the - // consumer, we'll continue scanning from that point. This way the - // scanning work is time-sliced. - nextFiber = null; - } else { - // Traverse down. - nextFiber = fiber.child; - } - break; - case ContextProvider: - // Don't scan deeper if this is a matching provider - nextFiber = fiber.type === workInProgress.type ? null : fiber.child; - break; - default: - // Traverse down. - nextFiber = fiber.child; - break; - } - if (nextFiber !== null) { - // Set the return pointer of the child to the work-in-progress fiber. - nextFiber.return = fiber; - } else { - // No child. Traverse to next sibling. - nextFiber = fiber; - while (nextFiber !== null) { - if (nextFiber === workInProgress) { - // We're back to the root of this subtree. Exit. - nextFiber = null; - break; - } - let sibling = nextFiber.sibling; - if (sibling !== null) { - // Set the return pointer of the sibling to the work-in-progress fiber. - sibling.return = nextFiber.return; - nextFiber = sibling; - break; - } - // No more siblings. Traverse up. - nextFiber = nextFiber.return; - } - } - fiber = nextFiber; - } -} - function updateContextProvider(current, workInProgress, renderExpirationTime) { const providerType: ReactProviderType = workInProgress.type; const context: ReactContext = providerType._context; @@ -1017,42 +922,18 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - const newValue = getContextCurrentValue(context); - const changedBits = getContextChangedBits(context); - + const hasPendingContext = prepareToReadContext( + workInProgress, + renderExpirationTime, + ); if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. - } else if (changedBits === 0 && oldProps === newProps) { + } else if (oldProps === newProps && !hasPendingContext) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } - workInProgress.memoizedProps = newProps; - - let observedBits = newProps.unstable_observedBits; - if (observedBits === undefined || observedBits === null) { - // Subscribe to all changes by default - observedBits = MAX_SIGNED_31_BIT_INT; - } - // Store the observedBits on the fiber's stateNode for quick access. - workInProgress.stateNode = observedBits; - if ((changedBits & observedBits) !== 0) { - // Context change propagation stops at matching consumers, for time- - // slicing. Continue the propagation here. - propagateContextChange( - workInProgress, - context, - changedBits, - renderExpirationTime, - ); - } else if (oldProps === newProps) { - // Skip over a memoized parent with a bitmask bailout even - // if we began working on it because of a deeper matching child. - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - // There is no bailout on `children` equality because we expect people - // to often pass a bound method as a child, but it may reference - // `this.state` or `this.props` (and thus needs to re-render on `setState`). + workInProgress.memoizedProps = newProps; const render = newProps.children; @@ -1066,6 +947,7 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { ); } + const newValue = readContext(context, newProps.unstable_observedBits); let newChildren; if (__DEV__) { ReactCurrentOwner.current = workInProgress; @@ -1107,6 +989,10 @@ function bailoutOnAlreadyFinishedWork( ): Fiber | null { cancelWorkTimer(workInProgress); + if (current !== null) { + workInProgress.firstContextDependency = current.firstContextDependency; + } + if (enableProfilerTimer) { // Don't update "base" render times for bailouts. stopBaseRenderTimerIfRunning(); diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 729f7469e24b5..3791c6725dbac 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -7,20 +7,25 @@ * @flow */ -import type {Fiber} from './ReactFiber'; import type {ReactContext} from 'shared/ReactTypes'; +import type {Fiber} from './ReactFiber'; import type {StackCursor} from './ReactFiberStack'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; -export type NewContext = { - pushProvider(providerFiber: Fiber): void, - popProvider(providerFiber: Fiber): void, - getContextCurrentValue(context: ReactContext): any, - getContextChangedBits(context: ReactContext): number, +export type ContextDependency = { + context: ReactContext, + observedBits: number, + next: ContextDependency | null, }; import warningWithoutStack from 'shared/warningWithoutStack'; import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack'; +import maxSigned31BitInt from './maxSigned31BitInt'; +import {NoWork} from './ReactFiberExpirationTime'; +import {ContextProvider} from 'shared/ReactTypeOfWork'; + +import invariant from 'shared/invariant'; const providerCursor: StackCursor = createCursor(null); const valueCursor: StackCursor = createCursor(null); @@ -32,7 +37,11 @@ if (__DEV__) { rendererSigil = {}; } -function pushProvider(providerFiber: Fiber): void { +let currentlyRenderingFiber: Fiber | null = null; +let lastContextDependency: ContextDependency | null = null; +let memoizedFirstContextDependency: ContextDependency | null = null; + +export function pushProvider(providerFiber: Fiber): void { const context: ReactContext = providerFiber.type._context; if (isPrimaryRenderer) { @@ -72,7 +81,7 @@ function pushProvider(providerFiber: Fiber): void { } } -function popProvider(providerFiber: Fiber): void { +export function popProvider(providerFiber: Fiber): void { const changedBits = changedBitsCursor.current; const currentValue = valueCursor.current; @@ -90,17 +99,191 @@ function popProvider(providerFiber: Fiber): void { } } -function getContextCurrentValue(context: ReactContext): any { - return isPrimaryRenderer ? context._currentValue : context._currentValue2; +export function propagateContextChange( + workInProgress: Fiber, + context: ReactContext, + changedBits: number, + renderExpirationTime: ExpirationTime, +): void { + let fiber = workInProgress.child; + if (fiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + fiber.return = workInProgress; + } + while (fiber !== null) { + let nextFiber; + + // Visit this fiber. + let dependency = fiber.firstContextDependency; + if (dependency !== null) { + do { + // Check if the context matches. + if ( + dependency.context === context && + (dependency.observedBits & changedBits) !== 0 + ) { + // Match! Update the expiration time of all the ancestors, including + // the alternates. + let node = fiber; + while (node !== null) { + const alternate = node.alternate; + if ( + node.expirationTime === NoWork || + node.expirationTime > renderExpirationTime + ) { + node.expirationTime = renderExpirationTime; + if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > renderExpirationTime) + ) { + alternate.expirationTime = renderExpirationTime; + } + } else if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > renderExpirationTime) + ) { + alternate.expirationTime = renderExpirationTime; + } else { + // Neither alternate was updated, which means the rest of the + // ancestor path already has sufficient priority. + break; + } + node = node.return; + } + // Don't scan deeper than a matching consumer. When we render the + // consumer, we'll continue scanning from that point. This way the + // scanning work is time-sliced. + nextFiber = null; + } else { + nextFiber = fiber.child; + } + dependency = dependency.next; + } while (dependency !== null); + } else if (fiber.tag === ContextProvider) { + // Don't scan deeper if this is a matching provider + nextFiber = fiber.type === workInProgress.type ? null : fiber.child; + } else { + // Traverse down. + nextFiber = fiber.child; + } + + if (nextFiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + nextFiber.return = fiber; + } else { + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + nextFiber = null; + break; + } + let sibling = nextFiber.sibling; + if (sibling !== null) { + // Set the return pointer of the sibling to the work-in-progress fiber. + sibling.return = nextFiber.return; + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; + } + } + fiber = nextFiber; + } } -function getContextChangedBits(context: ReactContext): number { - return isPrimaryRenderer ? context._changedBits : context._changedBits2; +export function prepareToReadContext( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): boolean { + currentlyRenderingFiber = workInProgress; + memoizedFirstContextDependency = workInProgress.firstContextDependency; + if (memoizedFirstContextDependency !== null) { + // Reset the work-in-progress list + workInProgress.firstContextDependency = null; + + // Iterate through the context dependencies and see if there were any + // changes. If so, continue propagating the context change by scanning + // the child subtree. + let dependency = memoizedFirstContextDependency; + let hasPendingContext = false; + do { + const context = dependency.context; + const changedBits = isPrimaryRenderer + ? context._changedBits + : context._changedBits2; + if (changedBits !== 0) { + // Resume context change propagation. We need to call this even if + // this fiber bails out, in case deeply nested consumers observe more + // bits than this one. + propagateContextChange( + workInProgress, + context, + changedBits, + renderExpirationTime, + ); + if ((changedBits & dependency.observedBits) !== 0) { + hasPendingContext = true; + } + } + dependency = dependency.next; + } while (dependency !== null); + return hasPendingContext; + } else { + return false; + } } -export { - pushProvider, - popProvider, - getContextCurrentValue, - getContextChangedBits, -}; +export function readContext( + context: ReactContext, + observedBits: void | number | boolean, +): T { + invariant( + currentlyRenderingFiber !== null, + 'Context.unstable_read(): Context can only be read while React is ' + + 'rendering, e.g. inside the render method or getDerivedStateFromProps.', + ); + + if (typeof observedBits !== 'number') { + if (observedBits === false) { + // Do not observe updates + observedBits = 0; + } else { + // Observe all updates + observedBits = maxSigned31BitInt; + } + } + + if (currentlyRenderingFiber.firstContextDependency === null) { + // This is the first dependency in the list + currentlyRenderingFiber.firstContextDependency = lastContextDependency = { + context: ((context: any): ReactContext), + observedBits, + next: null, + }; + } else { + invariant( + lastContextDependency !== null, + 'Expected a non-empty list of context dependencies. This is likely a ' + + 'bug in React. Please file an issue.', + ); + if (lastContextDependency.context === context) { + // Fast path. The previous context has the same type. We can reuse + // the same node. + lastContextDependency.observedBits |= observedBits; + } else { + // Append a new context item. + lastContextDependency = lastContextDependency.next = { + context: ((context: any): ReactContext), + observedBits, + next: null, + }; + } + } + + return isPrimaryRenderer ? context._currentValue : context._currentValue2; +} From d6c0f3eb4a671c70231554532cadb44774d7d2b3 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 2 Jul 2018 15:19:58 -0700 Subject: [PATCH 2/5] 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. --- .../src/ReactFiberBeginWork.js | 32 +- .../src/ReactFiberClassComponent.js | 54 +- .../src/ReactFiberDispatcher.js | 14 + .../src/ReactFiberScheduler.js | 3 + .../ReactNewContext-test.internal.js | 2099 +++++++++-------- packages/react/src/ReactContext.js | 18 + packages/react/src/ReactCurrentOwner.js | 2 + packages/shared/ReactTypes.js | 1 + 8 files changed, 1246 insertions(+), 977 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberDispatcher.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index e6386ba1d2485..1532815d87521 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -238,19 +238,25 @@ function markRef(current: Fiber | null, workInProgress: Fiber) { } } -function updateFunctionalComponent(current, workInProgress) { +function updateFunctionalComponent( + current, + workInProgress, + renderExpirationTime, +) { const fn = workInProgress.type; const nextProps = workInProgress.pendingProps; + const hasPendingContext = prepareToReadContext( + workInProgress, + renderExpirationTime, + ); if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. - } else { - if (workInProgress.memoizedProps === nextProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } + } else if (workInProgress.memoizedProps === nextProps && !hasPendingContext) { // TODO: consider bringing fn.shouldComponentUpdate() back. // It used to be here. + return bailoutOnAlreadyFinishedWork(current, workInProgress); } const unmaskedContext = getUnmaskedContext(workInProgress); @@ -266,6 +272,7 @@ function updateFunctionalComponent(current, workInProgress) { } else { nextChildren = fn(nextProps, context); } + // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren); @@ -282,6 +289,11 @@ function updateClassComponent( // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. const hasContext = pushLegacyContextProvider(workInProgress); + const hasPendingNewContext = prepareToReadContext( + workInProgress, + renderExpirationTime, + ); + let shouldUpdate; if (current === null) { if (workInProgress.stateNode === null) { @@ -298,6 +310,7 @@ function updateClassComponent( // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance( workInProgress, + hasPendingNewContext, renderExpirationTime, ); } @@ -305,6 +318,7 @@ function updateClassComponent( shouldUpdate = updateClassInstance( current, workInProgress, + hasPendingNewContext, renderExpirationTime, ); } @@ -581,6 +595,8 @@ function mountIndeterminateComponent( const unmaskedContext = getUnmaskedContext(workInProgress); const context = getMaskedContext(workInProgress, unmaskedContext); + prepareToReadContext(workInProgress, renderExpirationTime); + let value; if (__DEV__) { @@ -1082,7 +1098,11 @@ function beginWork( renderExpirationTime, ); case FunctionalComponent: - return updateFunctionalComponent(current, workInProgress); + return updateFunctionalComponent( + current, + workInProgress, + renderExpirationTime, + ); case ClassComponent: return updateClassComponent( current, diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 40485c97058a9..6ef80abb42af5 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -231,7 +231,7 @@ function checkShouldComponentUpdate( newProps, oldState, newState, - newContext, + nextLegacyContext, ) { const instance = workInProgress.stateNode; const ctor = workInProgress.type; @@ -240,7 +240,7 @@ function checkShouldComponentUpdate( const shouldUpdate = instance.shouldComponentUpdate( newProps, newState, - newContext, + nextLegacyContext, ); stopPhaseTimer(); @@ -620,15 +620,15 @@ function callComponentWillReceiveProps( workInProgress, instance, newProps, - newContext, + nextLegacyContext, ) { const oldState = instance.state; startPhaseTimer(workInProgress, 'componentWillReceiveProps'); if (typeof instance.componentWillReceiveProps === 'function') { - instance.componentWillReceiveProps(newProps, newContext); + instance.componentWillReceiveProps(newProps, nextLegacyContext); } if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') { - instance.UNSAFE_componentWillReceiveProps(newProps, newContext); + instance.UNSAFE_componentWillReceiveProps(newProps, nextLegacyContext); } stopPhaseTimer(); @@ -741,6 +741,7 @@ function mountClassInstance( function resumeMountClassInstance( workInProgress: Fiber, + hasPendingNewContext: boolean, renderExpirationTime: ExpirationTime, ): boolean { const ctor = workInProgress.type; @@ -751,8 +752,11 @@ function resumeMountClassInstance( instance.props = oldProps; const oldContext = instance.context; - const newUnmaskedContext = getUnmaskedContext(workInProgress); - const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress); + const nextLegacyContext = getMaskedContext( + workInProgress, + nextLegacyUnmaskedContext, + ); const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = @@ -770,12 +774,12 @@ function resumeMountClassInstance( (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function') ) { - if (oldProps !== newProps || oldContext !== newContext) { + if (oldProps !== newProps || oldContext !== nextLegacyContext) { callComponentWillReceiveProps( workInProgress, instance, newProps, - newContext, + nextLegacyContext, ); } } @@ -799,6 +803,7 @@ function resumeMountClassInstance( oldProps === newProps && oldState === newState && !hasContextChanged() && + !hasPendingNewContext && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update @@ -820,13 +825,14 @@ function resumeMountClassInstance( const shouldUpdate = checkHasForceUpdateAfterProcessing() || + hasPendingNewContext || checkShouldComponentUpdate( workInProgress, oldProps, newProps, oldState, newState, - newContext, + nextLegacyContext, ); if (shouldUpdate) { @@ -866,7 +872,7 @@ function resumeMountClassInstance( // if shouldComponentUpdate returns false. instance.props = newProps; instance.state = newState; - instance.context = newContext; + instance.context = nextLegacyContext; return shouldUpdate; } @@ -875,6 +881,7 @@ function resumeMountClassInstance( function updateClassInstance( current: Fiber, workInProgress: Fiber, + hasPendingNewContext: boolean, renderExpirationTime: ExpirationTime, ): boolean { const ctor = workInProgress.type; @@ -885,8 +892,11 @@ function updateClassInstance( instance.props = oldProps; const oldContext = instance.context; - const newUnmaskedContext = getUnmaskedContext(workInProgress); - const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress); + const nextLegacyContext = getMaskedContext( + workInProgress, + nextLegacyUnmaskedContext, + ); const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = @@ -904,12 +914,12 @@ function updateClassInstance( (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function') ) { - if (oldProps !== newProps || oldContext !== newContext) { + if (oldProps !== newProps || oldContext !== nextLegacyContext) { callComponentWillReceiveProps( workInProgress, instance, newProps, - newContext, + nextLegacyContext, ); } } @@ -934,6 +944,7 @@ function updateClassInstance( oldProps === newProps && oldState === newState && !hasContextChanged() && + !hasPendingNewContext && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update @@ -968,13 +979,14 @@ function updateClassInstance( const shouldUpdate = checkHasForceUpdateAfterProcessing() || + hasPendingNewContext || checkShouldComponentUpdate( workInProgress, oldProps, newProps, oldState, newState, - newContext, + nextLegacyContext, ); if (shouldUpdate) { @@ -987,10 +999,14 @@ function updateClassInstance( ) { startPhaseTimer(workInProgress, 'componentWillUpdate'); if (typeof instance.componentWillUpdate === 'function') { - instance.componentWillUpdate(newProps, newState, newContext); + instance.componentWillUpdate(newProps, newState, nextLegacyContext); } if (typeof instance.UNSAFE_componentWillUpdate === 'function') { - instance.UNSAFE_componentWillUpdate(newProps, newState, newContext); + instance.UNSAFE_componentWillUpdate( + newProps, + newState, + nextLegacyContext, + ); } stopPhaseTimer(); } @@ -1030,7 +1046,7 @@ function updateClassInstance( // if shouldComponentUpdate returns false. instance.props = newProps; instance.state = newState; - instance.context = newContext; + instance.context = nextLegacyContext; return shouldUpdate; } diff --git a/packages/react-reconciler/src/ReactFiberDispatcher.js b/packages/react-reconciler/src/ReactFiberDispatcher.js new file mode 100644 index 0000000000000..f515457770168 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberDispatcher.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {readContext} from './ReactFiberNewContext'; + +export const Dispatcher = { + readContext, +}; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 06b0041a76cf6..0c0004fc916d6 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -145,6 +145,7 @@ import { commitAttachRef, commitDetachRef, } from './ReactFiberCommitWork'; +import {Dispatcher} from './ReactFiberDispatcher'; export type Deadline = { timeRemaining: () => number, @@ -1017,6 +1018,7 @@ function renderRoot( 'by a bug in React. Please file an issue.', ); isWorking = true; + ReactCurrentOwner.currentDispatcher = Dispatcher; const expirationTime = root.nextExpirationTimeToWorkOn; @@ -1107,6 +1109,7 @@ function renderRoot( // We're done performing work. Time to clean up. isWorking = false; + ReactCurrentOwner.currentDispatcher = null; // Yield back to main thread. if (didFatal) { diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 09f9d6ec6f336..85cf6b6fbdaf8 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -30,1102 +30,1297 @@ describe('ReactNewContext', () => { // return {type: 'div', children, prop: undefined}; // } + function Text(props) { + ReactNoop.yield(props.text); + return ; + } + function span(prop) { return {type: 'span', children: [], prop}; } - it('simple mount and update', () => { - const Context = React.createContext(1); - - function Consumer(props) { - return ( - - {value => } - - ); - } - - const Indirection = React.Fragment; - - function App(props) { - return ( - - - - - - - - ); - } - - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); - - // Update - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]); - }); + // We have several ways of reading from context. sharedContextTests runs + // a suite of tests for a given context consumer implementation. + sharedContextTests('Context.Consumer', Context => Context.Consumer); + sharedContextTests( + 'Context.unstable_read inside functional component', + Context => + function Consumer(props) { + const observedBits = props.unstable_observedBits; + const contextValue = Context.unstable_read(observedBits); + const render = props.children; + return render(contextValue); + }, + ); + sharedContextTests( + 'Context.unstable_read inside class component', + Context => + class Consumer extends React.Component { + render() { + const observedBits = this.props.unstable_observedBits; + const contextValue = Context.unstable_read(observedBits); + const render = this.props.children; + return render(contextValue); + } + }, + ); - it('propagates through shouldComponentUpdate false', () => { - const Context = React.createContext(1); + function sharedContextTests(label, getConsumer) { + describe(`reading context with ${label}`, () => { + it('simple mount and update', () => { + const Context = React.createContext(1); + const Consumer = getConsumer(Context); - function Provider(props) { - ReactNoop.yield('Provider'); - return ( - - {props.children} - - ); - } + const Indirection = React.Fragment; - function Consumer(props) { - ReactNoop.yield('Consumer'); - return ( - - {value => { - ReactNoop.yield('Consumer render prop'); - return ; - }} - - ); - } + function App(props) { + return ( + + + + + {value => } + + + + + ); + } - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - ReactNoop.yield('Indirection'); - return this.props.children; - } - } + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); - function App(props) { - ReactNoop.yield('App'); - return ( - - - - - - - - ); - } + // Update + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]); + }); - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'App', - 'Provider', - 'Indirection', - 'Indirection', - 'Consumer', - 'Consumer render prop', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); - - // Update - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'App', - 'Provider', - 'Consumer render prop', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]); - }); + it('propagates through shouldComponentUpdate false', () => { + const Context = React.createContext(1); + const ContextConsumer = getConsumer(Context); - it('consumers bail out if context value is the same', () => { - const Context = React.createContext(1); + function Provider(props) { + ReactNoop.yield('Provider'); + return ( + + {props.children} + + ); + } - function Provider(props) { - ReactNoop.yield('Provider'); - return ( - - {props.children} - - ); - } + function Consumer(props) { + ReactNoop.yield('Consumer'); + return ( + + {value => { + ReactNoop.yield('Consumer render prop'); + return ; + }} + + ); + } - function Consumer(props) { - ReactNoop.yield('Consumer'); - return ( - - {value => { - ReactNoop.yield('Consumer render prop'); - return ; - }} - - ); - } + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + ReactNoop.yield('Indirection'); + return this.props.children; + } + } - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - ReactNoop.yield('Indirection'); - return this.props.children; - } - } + function App(props) { + ReactNoop.yield('App'); + return ( + + + + + + + + ); + } - function App(props) { - ReactNoop.yield('App'); - return ( - - - - - - - - ); - } + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'App', + 'Provider', + 'Indirection', + 'Indirection', + 'Consumer', + 'Consumer render prop', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); + + // Update + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'App', + 'Provider', + 'Consumer render prop', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]); + }); + + it('consumers bail out if context value is the same', () => { + const Context = React.createContext(1); + const ContextConsumer = getConsumer(Context); + + function Provider(props) { + ReactNoop.yield('Provider'); + return ( + + {props.children} + + ); + } - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'App', - 'Provider', - 'Indirection', - 'Indirection', - 'Consumer', - 'Consumer render prop', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); - - // Update with the same context value - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'App', - 'Provider', - // Don't call render prop again - ]); - expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); - }); + function Consumer(props) { + ReactNoop.yield('Consumer'); + return ( + + {value => { + ReactNoop.yield('Consumer render prop'); + return ; + }} + + ); + } - it('nested providers', () => { - const Context = React.createContext(1); + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + ReactNoop.yield('Indirection'); + return this.props.children; + } + } - function Provider(props) { - return ( - - {contextValue => ( - // Multiply previous context value by 2, unless prop overrides - - {props.children} - - )} - - ); - } + function App(props) { + ReactNoop.yield('App'); + return ( + + + + + + + + ); + } - function Consumer(props) { - return ( - - {value => } - - ); - } + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'App', + 'Provider', + 'Indirection', + 'Indirection', + 'Consumer', + 'Consumer render prop', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); + + // Update with the same context value + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'App', + 'Provider', + // Don't call render prop again + ]); + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); + }); + + it('nested providers', () => { + const Context = React.createContext(1); + const Consumer = getConsumer(Context); + + function Provider(props) { + return ( + + {contextValue => ( + // Multiply previous context value by 2, unless prop overrides + + {props.children} + + )} + + ); + } - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - return this.props.children; - } - } + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } - function App(props) { - return ( - - - + function App(props) { + return ( + - + + + + {value => } + + + - - - ); - } + ); + } - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]); + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]); - // Update - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]); - }); + // Update + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]); + }); - it('should provide the correct (default) values to consumers outside of a provider', () => { - const FooContext = React.createContext({value: 'foo-initial'}); - const BarContext = React.createContext({value: 'bar-initial'}); + it('should provide the correct (default) values to consumers outside of a provider', () => { + const FooContext = React.createContext({value: 'foo-initial'}); + const BarContext = React.createContext({value: 'bar-initial'}); + const FooConsumer = getConsumer(FooContext); + const BarConsumer = getConsumer(BarContext); - const Verify = ({actual, expected}) => { - expect(expected).toBe(actual); - return null; - }; + const Verify = ({actual, expected}) => { + expect(expected).toBe(actual); + return null; + }; - ReactNoop.render( - - - - {({value}) => } - - - - - {({value}) => } - - - - - - {({value}) => } - - - {({value}) => } - - , - ); - ReactNoop.flush(); - }); + ReactNoop.render( + + + + {({value}) => } + + + + + {({value}) => ( + + )} + + + + + + {({value}) => } + + + {({value}) => } + + , + ); + ReactNoop.flush(); + }); + + it('multiple consumers in different branches', () => { + const Context = React.createContext(1); + const Consumer = getConsumer(Context); + + function Provider(props) { + return ( + + {contextValue => ( + // Multiply previous context value by 2, unless prop overrides + + {props.children} + + )} + + ); + } - it('multiple consumers in different branches', () => { - const Context = React.createContext(1); + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } - function Provider(props) { - return ( - - {contextValue => ( - // Multiply previous context value by 2, unless prop overrides - - {props.children} - - )} - - ); - } + function App(props) { + return ( + + + + + + {value => } + + + + + + {value => } + + + + + ); + } - function Consumer(props) { - return ( - - {value => } - - ); - } + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([ + span('Result: 4'), + span('Result: 2'), + ]); - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - return this.props.children; - } - } + // Update + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([ + span('Result: 6'), + span('Result: 3'), + ]); - function App(props) { - return ( - - - - - - - - - - - - - ); - } + // Another update + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([ + span('Result: 8'), + span('Result: 4'), + ]); + }); + + it('compares context values with Object.is semantics', () => { + const Context = React.createContext(1); + const ContextConsumer = getConsumer(Context); + + function Provider(props) { + ReactNoop.yield('Provider'); + return ( + + {props.children} + + ); + } - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - span('Result: 4'), - span('Result: 2'), - ]); + function Consumer(props) { + ReactNoop.yield('Consumer'); + return ( + + {value => { + ReactNoop.yield('Consumer render prop'); + return ; + }} + + ); + } - // Update - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - span('Result: 6'), - span('Result: 3'), - ]); + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + ReactNoop.yield('Indirection'); + return this.props.children; + } + } - // Another update - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - span('Result: 8'), - span('Result: 4'), - ]); - }); + function App(props) { + ReactNoop.yield('App'); + return ( + + + + + + + + ); + } - it('compares context values with Object.is semantics', () => { - const Context = React.createContext(1); + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'App', + 'Provider', + 'Indirection', + 'Indirection', + 'Consumer', + 'Consumer render prop', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]); + + // Update + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'App', + 'Provider', + // Consumer should not re-render again + // 'Consumer render prop', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]); + }); + + it('context unwinds when interrupted', () => { + const Context = React.createContext('Default'); + const ContextConsumer = getConsumer(Context); + + function Consumer(props) { + return ( + + {value => } + + ); + } - function Provider(props) { - ReactNoop.yield('Provider'); - return ( - - {props.children} - - ); - } + function BadRender() { + throw new Error('Bad render'); + } - function Consumer(props) { - ReactNoop.yield('Consumer'); - return ( - - {value => { - ReactNoop.yield('Consumer render prop'); - return ; - }} - - ); - } + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return null; + } + return this.props.children; + } + } - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - ReactNoop.yield('Indirection'); - return this.props.children; - } - } + function App(props) { + return ( + + + + + + + + + + + ); + } - function App(props) { - ReactNoop.yield('App'); - return ( - - - - - - - - ); - } + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([ + // The second provider should use the default value. + span('Result: Does not unwind'), + ]); + }); + + it('can skip consumers with bitmask', () => { + const Context = React.createContext({foo: 0, bar: 0}, (a, b) => { + let result = 0; + if (a.foo !== b.foo) { + result |= 0b01; + } + if (a.bar !== b.bar) { + result |= 0b10; + } + return result; + }); + const Consumer = getConsumer(Context); - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'App', - 'Provider', - 'Indirection', - 'Indirection', - 'Consumer', - 'Consumer render prop', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]); - - // Update - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'App', - 'Provider', - // Consumer should not re-render again - // 'Consumer render prop', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]); - }); + function Provider(props) { + return ( + + {props.children} + + ); + } - it('context unwinds when interrupted', () => { - const Context = React.createContext('Default'); + function Foo() { + return ( + + {value => { + ReactNoop.yield('Foo'); + return ; + }} + + ); + } - function Consumer(props) { - return ( - - {value => } - - ); - } + function Bar() { + return ( + + {value => { + ReactNoop.yield('Bar'); + return ; + }} + + ); + } - function BadRender() { - throw new Error('Bad render'); - } + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - if (this.state.error) { - return null; + function App(props) { + return ( + + + + + + + + + + + ); } - return this.props.children; - } - } - function App(props) { - return ( - - - - - - - - - - - ); - } + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 1'), + span('Bar: 1'), + ]); + + // Update only foo + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2'), + span('Bar: 1'), + ]); + + // Update only bar + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Bar']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2'), + span('Bar: 2'), + ]); + + // Update both + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 3'), + span('Bar: 3'), + ]); + }); + + it('can skip parents with bitmask bailout while updating their children', () => { + const Context = React.createContext({foo: 0, bar: 0}, (a, b) => { + let result = 0; + if (a.foo !== b.foo) { + result |= 0b01; + } + if (a.bar !== b.bar) { + result |= 0b10; + } + return result; + }); + const Consumer = getConsumer(Context); - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - // The second provider should use the default value. - span('Result: Does not unwind'), - ]); - }); + function Provider(props) { + return ( + + {props.children} + + ); + } - it('can skip consumers with bitmask', () => { - const Context = React.createContext({foo: 0, bar: 0}, (a, b) => { - let result = 0; - if (a.foo !== b.foo) { - result |= 0b01; - } - if (a.bar !== b.bar) { - result |= 0b10; - } - return result; - }); + function Foo(props) { + return ( + + {value => { + ReactNoop.yield('Foo'); + return ( + + + {props.children && props.children()} + + ); + }} + + ); + } - function Provider(props) { - return ( - - {props.children} - - ); - } + function Bar(props) { + return ( + + {value => { + ReactNoop.yield('Bar'); + return ( + + + {props.children && props.children()} + + ); + }} + + ); + } - function Foo() { - return ( - - {value => { - ReactNoop.yield('Foo'); - return ; - }} - - ); - } + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } - function Bar() { - return ( - - {value => { - ReactNoop.yield('Bar'); - return ; - }} - - ); - } + function App(props) { + return ( + + + + {/* Use a render prop so we don't test constant elements. */} + {() => ( + + + {() => ( + + + + )} + + + )} + + + + ); + } - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - return this.props.children; - } - } + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 1'), + span('Bar: 1'), + span('Foo: 1'), + ]); + + // Update only foo + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo', 'Foo']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2'), + span('Bar: 1'), + span('Foo: 2'), + ]); + + // Update only bar + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Bar']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2'), + span('Bar: 2'), + span('Foo: 2'), + ]); + + // Update both + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 3'), + span('Bar: 3'), + span('Foo: 3'), + ]); + }); + + it("does not re-render if there's an update in a child", () => { + const Context = React.createContext(0); + const Consumer = getConsumer(Context); + + let child; + class Child extends React.Component { + state = {step: 0}; + render() { + ReactNoop.yield('Child'); + return ( + + ); + } + } - function App(props) { - return ( - - - - - - - - - - - ); - } + function App(props) { + return ( + + + {value => { + ReactNoop.yield('Consumer render prop'); + return (child = inst)} context={value} />; + }} + + + ); + } - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']); - expect(ReactNoop.getChildren()).toEqual([span('Foo: 1'), span('Bar: 1')]); + // Initial mount + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']); + expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]); - // Update only foo - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Foo']); - expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 1')]); + child.setState({step: 1}); + expect(ReactNoop.flush()).toEqual(['Child']); + expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]); + }); - // Update only bar - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Bar']); - expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 2')]); + it('consumer bails out if value is unchanged and something above bailed out', () => { + const Context = React.createContext(0); + const Consumer = getConsumer(Context); - // Update both - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']); - expect(ReactNoop.getChildren()).toEqual([span('Foo: 3'), span('Bar: 3')]); - }); + function renderChildValue(value) { + ReactNoop.yield('Consumer'); + return ; + } - it('can skip parents with bitmask bailout while updating their children', () => { - const Context = React.createContext({foo: 0, bar: 0}, (a, b) => { - let result = 0; - if (a.foo !== b.foo) { - result |= 0b01; - } - if (a.bar !== b.bar) { - result |= 0b10; - } - return result; - }); + function ChildWithInlineRenderCallback() { + ReactNoop.yield('ChildWithInlineRenderCallback'); + // Note: we are intentionally passing an inline arrow. Don't refactor. + return {value => renderChildValue(value)}; + } - function Provider(props) { - return ( - - {props.children} - - ); - } + function ChildWithCachedRenderCallback() { + ReactNoop.yield('ChildWithCachedRenderCallback'); + return {renderChildValue}; + } - function Foo(props) { - return ( - - {value => { - ReactNoop.yield('Foo'); + class PureIndirection extends React.PureComponent { + render() { + ReactNoop.yield('PureIndirection'); return ( - - {props.children && props.children()} + + ); - }} - - ); - } + } + } - function Bar(props) { - return ( - - {value => { - ReactNoop.yield('Bar'); + class App extends React.Component { + render() { + ReactNoop.yield('App'); return ( - - - {props.children && props.children()} - + + + ); - }} - - ); - } + } + } - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - return this.props.children; - } - } + // Initial mount + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'App', + 'PureIndirection', + 'ChildWithInlineRenderCallback', + 'Consumer', + 'ChildWithCachedRenderCallback', + 'Consumer', + ]); + expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]); + + // Update (bailout) + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['App']); + expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]); + + // Update (no bailout) + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']); + expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]); + }); + + // Context consumer bails out on propagating "deep" updates when `value` hasn't changed. + // However, it doesn't bail out from rendering if the component above it re-rendered anyway. + // If we bailed out on referential equality, it would be confusing that you + // can call this.setState(), but an autobound render callback "blocked" the update. + // https://github.com/facebook/react/pull/12470#issuecomment-376917711 + it('consumer does not bail out if there were no bailouts above it', () => { + const Context = React.createContext(0); + const Consumer = getConsumer(Context); + + class App extends React.Component { + state = { + text: 'hello', + }; + + renderConsumer = context => { + ReactNoop.yield('App#renderConsumer'); + return ; + }; + + render() { + ReactNoop.yield('App'); + return ( + + {this.renderConsumer} + + ); + } + } - function App(props) { - return ( - - - - {/* Use a render prop so we don't test constant elements. */} - {() => ( - - - {() => ( - - - - )} - - - )} - - - - ); - } + // Initial mount + let inst; + ReactNoop.render( (inst = ref)} />); + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']); + expect(ReactNoop.getChildren()).toEqual([span('hello')]); + + // Update + inst.setState({text: 'goodbye'}); + expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']); + expect(ReactNoop.getChildren()).toEqual([span('goodbye')]); + }); + + // This is a regression case for https://github.com/facebook/react/issues/12389. + it('does not run into an infinite loop', () => { + const Context = React.createContext(null); + const Consumer = getConsumer(Context); + + class App extends React.Component { + renderItem(id) { + return ( + + {() => inner} + outer + + ); + } + renderList() { + const list = [1, 2].map(id => this.renderItem(id)); + if (this.props.reverse) { + list.reverse(); + } + return list; + } + render() { + return ( + + {this.renderList()} + + ); + } + } - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']); - expect(ReactNoop.getChildren()).toEqual([ - span('Foo: 1'), - span('Bar: 1'), - span('Foo: 1'), - ]); - - // Update only foo - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Foo', 'Foo']); - expect(ReactNoop.getChildren()).toEqual([ - span('Foo: 2'), - span('Bar: 1'), - span('Foo: 2'), - ]); - - // Update only bar - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Bar']); - expect(ReactNoop.getChildren()).toEqual([ - span('Foo: 2'), - span('Bar: 2'), - span('Foo: 2'), - ]); - - // Update both - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']); - expect(ReactNoop.getChildren()).toEqual([ - span('Foo: 3'), - span('Bar: 3'), - span('Foo: 3'), - ]); - }); + ReactNoop.render(); + ReactNoop.flush(); + ReactNoop.render(); + ReactNoop.flush(); + ReactNoop.render(); + ReactNoop.flush(); + }); - it('warns if calculateChangedBits returns larger than a 31-bit integer', () => { - const Context = React.createContext( - 0, - (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int - ); + // This is a regression case for https://github.com/facebook/react/issues/12686 + it('does not skip some siblings', () => { + const Context = React.createContext(0); + const ContextConsumer = getConsumer(Context); - function App(props) { - return ; - } + class App extends React.Component { + state = { + step: 0, + }; - ReactNoop.render(); - ReactNoop.flush(); + render() { + ReactNoop.yield('App'); + return ( + + + {this.state.step > 0 && } + + ); + } + } - // Update - ReactNoop.render(); - expect(ReactNoop.flush).toWarnDev( - 'calculateChangedBits: Expected the return value to be a 31-bit ' + - 'integer. Instead received: 4294967295', - ); - }); + class StaticContent extends React.PureComponent { + render() { + return ( + + + + + + + ); + } + } - it('warns if multiple renderers concurrently render the same context', () => { - spyOnDev(console, 'error'); - const Context = React.createContext(0); + class Indirection extends React.PureComponent { + render() { + return ( + + {value => { + ReactNoop.yield('Consumer'); + return ; + }} + + ); + } + } - function Foo(props) { - ReactNoop.yield('Foo'); - return null; - } + // Initial mount + let inst; + ReactNoop.render( (inst = ref)} />); + expect(ReactNoop.flush()).toEqual(['App']); + expect(ReactNoop.getChildren()).toEqual([ + span('static 1'), + span('static 2'), + ]); + // Update the first time + inst.setState({step: 1}); + expect(ReactNoop.flush()).toEqual(['App', 'Consumer']); + expect(ReactNoop.getChildren()).toEqual([ + span('static 1'), + span('static 2'), + span(1), + ]); + // Update the second time + inst.setState({step: 2}); + expect(ReactNoop.flush()).toEqual(['App', 'Consumer']); + expect(ReactNoop.getChildren()).toEqual([ + span('static 1'), + span('static 2'), + span(2), + ]); + }); + }); + } - function App(props) { - return ( - - - - + describe('Context.Provider', () => { + it('warns if calculateChangedBits returns larger than a 31-bit integer', () => { + const Context = React.createContext( + 0, + (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int ); - } - ReactNoop.render(); - // Render past the Provider, but don't commit yet - ReactNoop.flushThrough(['Foo']); - - // Get a new copy of ReactNoop - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - React = require('react'); - ReactNoop = require('react-noop-renderer'); + function App(props) { + return ; + } - // Render the provider again using a different renderer - ReactNoop.render(); - ReactNoop.flush(); + ReactNoop.render(); + ReactNoop.flush(); - if (__DEV__) { - expect(console.error.calls.argsFor(0)[0]).toContain( - 'Detected multiple renderers concurrently rendering the same ' + - 'context provider. This is currently unsupported', + // Update + ReactNoop.render(); + expect(ReactNoop.flush).toWarnDev( + 'calculateChangedBits: Expected the return value to be a 31-bit ' + + 'integer. Instead received: 4294967295', ); - } - }); + }); - it('warns if consumer child is not a function', () => { - spyOnDev(console, 'error'); - const Context = React.createContext(0); - ReactNoop.render(); - expect(ReactNoop.flush).toThrow('render is not a function'); - if (__DEV__) { - expect(console.error.calls.argsFor(0)[0]).toContain( - 'A context consumer was rendered with multiple children, or a child ' + - "that isn't a function", - ); - } - }); + it('warns if multiple renderers concurrently render the same context', () => { + spyOnDev(console, 'error'); + const Context = React.createContext(0); - it("does not re-render if there's an update in a child", () => { - const Context = React.createContext(0); + function Foo(props) { + ReactNoop.yield('Foo'); + return null; + } - let child; - class Child extends React.Component { - state = {step: 0}; - render() { - ReactNoop.yield('Child'); + function App(props) { return ( - + + + + ); } - } - - function App(props) { - return ( - - - {value => { - ReactNoop.yield('Consumer render prop'); - return (child = inst)} context={value} />; - }} - - - ); - } - - // Initial mount - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']); - expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]); - - child.setState({step: 1}); - expect(ReactNoop.flush()).toEqual(['Child']); - expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]); - }); - - it('provider bails out if children and value are unchanged (like sCU)', () => { - const Context = React.createContext(0); - function Child() { - ReactNoop.yield('Child'); - return ; - } - - const children = ; - - function App(props) { - ReactNoop.yield('App'); - return ( - {children} - ); - } + ReactNoop.render(); + // Render past the Provider, but don't commit yet + ReactNoop.flushThrough(['Foo']); - // Initial mount - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['App', 'Child']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - - // Update - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'App', - // Child does not re-render - ]); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - }); + // Get a new copy of ReactNoop + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + React = require('react'); + ReactNoop = require('react-noop-renderer'); - it('provider does not bail out if legacy context changed above', () => { - const Context = React.createContext(0); + // Render the provider again using a different renderer + ReactNoop.render(); + ReactNoop.flush(); - function Child() { - ReactNoop.yield('Child'); - return ; - } + if (__DEV__) { + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Detected multiple renderers concurrently rendering the same ' + + 'context provider. This is currently unsupported', + ); + } + }); - const children = ; + it('provider bails out if children and value are unchanged (like sCU)', () => { + const Context = React.createContext(0); - class LegacyProvider extends React.Component { - static childContextTypes = { - legacyValue: () => {}, - }; - state = {legacyValue: 1}; - getChildContext() { - return {legacyValue: this.state.legacyValue}; - } - render() { - ReactNoop.yield('LegacyProvider'); - return this.props.children; + function Child() { + ReactNoop.yield('Child'); + return ; } - } - class App extends React.Component { - state = {value: 1}; - render() { + const children = ; + + function App(props) { ReactNoop.yield('App'); return ( - - {this.props.children} - + {children} ); } - } - - const legacyProviderRef = React.createRef(); - const appRef = React.createRef(); - - // Initial mount - ReactNoop.render( - - - {children} - - , - ); - expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - - // Update App with same value (should bail out) - appRef.current.setState({value: 1}); - expect(ReactNoop.flush()).toEqual(['App']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - - // Update LegacyProvider (should not bail out) - legacyProviderRef.current.setState({value: 1}); - expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - - // Update App with same value (should bail out) - appRef.current.setState({value: 1}); - expect(ReactNoop.flush()).toEqual(['App']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - }); - it('consumer bails out if value is unchanged and something above bailed out', () => { - const Context = React.createContext(0); + // Initial mount + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['App', 'Child']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + + // Update + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'App', + // Child does not re-render + ]); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + }); - function renderChildValue(value) { - ReactNoop.yield('Consumer'); - return ; - } + it('provider does not bail out if legacy context changed above', () => { + const Context = React.createContext(0); - function ChildWithInlineRenderCallback() { - ReactNoop.yield('ChildWithInlineRenderCallback'); - // Note: we are intentionally passing an inline arrow. Don't refactor. - return ( - {value => renderChildValue(value)} - ); - } + function Child() { + ReactNoop.yield('Child'); + return ; + } - function ChildWithCachedRenderCallback() { - ReactNoop.yield('ChildWithCachedRenderCallback'); - return {renderChildValue}; - } + const children = ; - class PureIndirection extends React.PureComponent { - render() { - ReactNoop.yield('PureIndirection'); - return ( - - - - - ); + class LegacyProvider extends React.Component { + static childContextTypes = { + legacyValue: () => {}, + }; + state = {legacyValue: 1}; + getChildContext() { + return {legacyValue: this.state.legacyValue}; + } + render() { + ReactNoop.yield('LegacyProvider'); + return this.props.children; + } } - } - class App extends React.Component { - render() { - ReactNoop.yield('App'); - return ( - - - - ); + class App extends React.Component { + state = {value: 1}; + render() { + ReactNoop.yield('App'); + return ( + + {this.props.children} + + ); + } } - } - // Initial mount - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual([ - 'App', - 'PureIndirection', - 'ChildWithInlineRenderCallback', - 'Consumer', - 'ChildWithCachedRenderCallback', - 'Consumer', - ]); - expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]); - - // Update (bailout) - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['App']); - expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]); - - // Update (no bailout) - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']); - expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]); - }); - - // Context consumer bails out on propagating "deep" updates when `value` hasn't changed. - // However, it doesn't bail out from rendering if the component above it re-rendered anyway. - // If we bailed out on referential equality, it would be confusing that you - // can call this.setState(), but an autobound render callback "blocked" the update. - // https://github.com/facebook/react/pull/12470#issuecomment-376917711 - it('consumer does not bail out if there were no bailouts above it', () => { - const Context = React.createContext(0); + const legacyProviderRef = React.createRef(); + const appRef = React.createRef(); - class App extends React.Component { - state = { - text: 'hello', - }; - - renderConsumer = context => { - ReactNoop.yield('App#renderConsumer'); - return ; - }; + // Initial mount + ReactNoop.render( + + + {children} + + , + ); + expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + + // Update App with same value (should bail out) + appRef.current.setState({value: 1}); + expect(ReactNoop.flush()).toEqual(['App']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + + // Update LegacyProvider (should not bail out) + legacyProviderRef.current.setState({value: 1}); + expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + + // Update App with same value (should bail out) + appRef.current.setState({value: 1}); + expect(ReactNoop.flush()).toEqual(['App']); + expect(ReactNoop.getChildren()).toEqual([span('Child')]); + }); + }); - render() { - ReactNoop.yield('App'); - return ( - - {this.renderConsumer} - + describe('Context.Consumer', () => { + it('warns if child is not a function', () => { + spyOnDev(console, 'error'); + const Context = React.createContext(0); + ReactNoop.render(); + expect(ReactNoop.flush).toThrow('render is not a function'); + if (__DEV__) { + expect(console.error.calls.argsFor(0)[0]).toContain( + 'A context consumer was rendered with multiple children, or a child ' + + "that isn't a function", ); } - } - - // Initial mount - let inst; - ReactNoop.render( (inst = ref)} />); - expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']); - expect(ReactNoop.getChildren()).toEqual([span('hello')]); - - // Update - inst.setState({text: 'goodbye'}); - expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']); - expect(ReactNoop.getChildren()).toEqual([span('goodbye')]); - }); + }); - // This is a regression case for https://github.com/facebook/react/issues/12389. - it('does not run into an infinite loop', () => { - const Context = React.createContext(null); + it('can read other contexts inside consumer render prop', () => { + const FooContext = React.createContext(0); + const BarContext = React.createContext(0); - class App extends React.Component { - renderItem(id) { + function FooAndBar() { return ( - - {() => inner} - outer - + + {foo => { + const bar = BarContext.unstable_read(); + return ; + }} + ); } - renderList() { - const list = [1, 2].map(id => this.renderItem(id)); - if (this.props.reverse) { - list.reverse(); + + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; } - return list; } - render() { + + function App(props) { return ( - {this.renderList()} + + + + + + + ); } - } - ReactNoop.render(); - ReactNoop.flush(); - ReactNoop.render(); - ReactNoop.flush(); - ReactNoop.render(); - ReactNoop.flush(); - }); + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Foo: 1, Bar: 1')]); - // This is a regression case for https://github.com/facebook/react/issues/12686 - it('does not skip some siblings', () => { - const Context = React.createContext(0); + // Update foo + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 1')]); - class App extends React.Component { - state = { - step: 0, - }; + // Update bar + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']); + expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 2')]); + }); + }); - render() { - ReactNoop.yield('App'); + describe('unstable_readContext', () => { + it('can use the same context multiple times in the same function', () => { + const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => { + let result = 0; + if (a.foo !== b.foo) { + result |= 0b001; + } + if (a.bar !== b.bar) { + result |= 0b010; + } + if (a.baz !== b.baz) { + result |= 0b100; + } + return result; + }); + + function Provider(props) { return ( - - - {this.state.step > 0 && } + + {props.children} ); } - } - class StaticContent extends React.PureComponent { - render() { - return ( - - - - - - - ); + function FooAndBar() { + const {foo} = Context.unstable_read(0b001); + const {bar} = Context.unstable_read(0b010); + return ; } - } - class Indirection extends React.PureComponent { - render() { - return ; + function Baz() { + const {baz} = Context.unstable_read(0b100); + return ; } - } - function Consumer() { - return ( - - {value => { - ReactNoop.yield('Consumer'); - return ; - }} - - ); - } + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } - // Initial mount - let inst; - ReactNoop.render( (inst = ref)} />); - expect(ReactNoop.flush()).toEqual(['App']); - expect(ReactNoop.getChildren()).toEqual([ - span('static 1'), - span('static 2'), - ]); - // Update the first time - inst.setState({step: 1}); - expect(ReactNoop.flush()).toEqual(['App', 'Consumer']); - expect(ReactNoop.getChildren()).toEqual([ - span('static 1'), - span('static 2'), - span(1), - ]); - // Update the second time - inst.setState({step: 2}); - expect(ReactNoop.flush()).toEqual(['App', 'Consumer']); - expect(ReactNoop.getChildren()).toEqual([ - span('static 1'), - span('static 2'), - span(2), - ]); + function App(props) { + return ( + + + + + + + + + + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1', 'Baz: 1']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 1, Bar: 1'), + span('Baz: 1'), + ]); + + // Update only foo + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2, Bar: 1'), + span('Baz: 1'), + ]); + + // Update only bar + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2, Bar: 2'), + span('Baz: 1'), + ]); + + // Update only baz + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Baz: 2']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2, Bar: 2'), + span('Baz: 2'), + ]); + }); }); it('unwinds after errors in complete phase', () => { diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 69461b7a9fe19..02ea7959af535 100644 --- a/packages/react/src/ReactContext.js +++ b/packages/react/src/ReactContext.js @@ -11,8 +11,24 @@ import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; +import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; +import ReactCurrentOwner from './ReactCurrentOwner'; + +export function readContext( + context: ReactContext, + observedBits: void | number | boolean, +): T { + const dispatcher = ReactCurrentOwner.currentDispatcher; + invariant( + dispatcher !== null, + 'Context.unstable_read(): Context can only be read while React is ' + + 'rendering, e.g. inside the render method or getDerivedStateFromProps.', + ); + return dispatcher.readContext(context, observedBits); +} + export function createContext( defaultValue: T, calculateChangedBits: ?(a: T, b: T) => number, @@ -47,6 +63,7 @@ export function createContext( // These are circular Provider: (null: any), Consumer: (null: any), + unstable_read: (null: any), }; context.Provider = { @@ -54,6 +71,7 @@ export function createContext( _context: context, }; context.Consumer = context; + context.unstable_read = readContext.bind(null, context); if (__DEV__) { context._currentRenderer = null; diff --git a/packages/react/src/ReactCurrentOwner.js b/packages/react/src/ReactCurrentOwner.js index 72ed4e2eb8475..89cd104ca6a9d 100644 --- a/packages/react/src/ReactCurrentOwner.js +++ b/packages/react/src/ReactCurrentOwner.js @@ -8,6 +8,7 @@ */ import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import typeof {Dispatcher} from 'react-reconciler/src/ReactFiberDispatcher'; /** * Keeps track of the current owner. @@ -21,6 +22,7 @@ const ReactCurrentOwner = { * @type {ReactComponent} */ current: (null: null | Fiber), + currentDispatcher: (null: null | Dispatcher), }; export default ReactCurrentOwner; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index ac4eed362d0ad..bff9413c82854 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -79,6 +79,7 @@ export type ReactContext = { $$typeof: Symbol | number, Consumer: ReactContext, Provider: ReactProviderType, + unstable_read: () => T, _calculateChangedBits: ((a: T, b: T) => number) | null, _defaultValue: T, From 2c7da10a5d4d91465093754a38b67866f793f3f3 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 16 Jul 2018 13:55:14 -0700 Subject: [PATCH 3/5] Remove vestigial context cursor Wasn't being used. --- packages/react-reconciler/src/ReactFiberNewContext.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 3791c6725dbac..92035588440e4 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -27,7 +27,6 @@ import {ContextProvider} from 'shared/ReactTypeOfWork'; import invariant from 'shared/invariant'; -const providerCursor: StackCursor = createCursor(null); const valueCursor: StackCursor = createCursor(null); const changedBitsCursor: StackCursor = createCursor(0); @@ -47,7 +46,6 @@ export function pushProvider(providerFiber: Fiber): void { if (isPrimaryRenderer) { push(changedBitsCursor, context._changedBits, providerFiber); push(valueCursor, context._currentValue, providerFiber); - push(providerCursor, providerFiber, providerFiber); context._currentValue = providerFiber.pendingProps.value; context._changedBits = providerFiber.stateNode; @@ -64,7 +62,6 @@ export function pushProvider(providerFiber: Fiber): void { } else { push(changedBitsCursor, context._changedBits2, providerFiber); push(valueCursor, context._currentValue2, providerFiber); - push(providerCursor, providerFiber, providerFiber); context._currentValue2 = providerFiber.pendingProps.value; context._changedBits2 = providerFiber.stateNode; @@ -85,7 +82,6 @@ export function popProvider(providerFiber: Fiber): void { const changedBits = changedBitsCursor.current; const currentValue = valueCursor.current; - pop(providerCursor, providerFiber); pop(valueCursor, providerFiber); pop(changedBitsCursor, providerFiber); From 789d88515040a88f4b424ac732d62f14b1a11a79 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 16 Jul 2018 14:09:25 -0700 Subject: [PATCH 4/5] Split fiber.expirationTime into two separate fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/createReactNoop.js | 5 +- packages/react-reconciler/src/ReactFiber.js | 17 +- .../src/ReactFiberBeginWork.js | 483 +++++++++--------- .../src/ReactFiberClassComponent.js | 8 +- .../src/ReactFiberNewContext.js | 39 +- .../src/ReactFiberReconciler.js | 2 +- .../src/ReactFiberScheduler.js | 138 +++-- .../src/ReactFiberUnwindWork.js | 11 +- .../react-reconciler/src/ReactUpdateQueue.js | 52 +- 9 files changed, 389 insertions(+), 366 deletions(-) diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 1505beb08339b..81f2081f2fe4a 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -662,7 +662,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { '- ' + // need to explicitly coerce Symbol to a string (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'), - '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', + '[' + + fiber.childExpirationTime + + (fiber.pendingProps ? '*' : '') + + ']', ); if (fiber.updateQueue) { logUpdateQueue(fiber.updateQueue, depth); diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 06f4f7a4c81e8..a39f5f3328f55 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -149,9 +149,12 @@ export type Fiber = {| lastEffect: Fiber | null, // Represents a time in the future by which this work should be completed. - // This is also used to quickly determine if a subtree has no pending changes. + // Does not include work found in its subtree. expirationTime: ExpirationTime, + // This is used to quickly determine if a subtree has no pending changes. + childExpirationTime: ExpirationTime, + // This is a pooled version of a Fiber. Every fiber that gets updated will // eventually have a pair. There are cases when we can clean up pairs to save // memory if we need to. @@ -229,6 +232,7 @@ function FiberNode( this.lastEffect = null; this.expirationTime = NoWork; + this.childExpirationTime = NoWork; this.alternate = null; @@ -330,7 +334,15 @@ export function createWorkInProgress( } } - workInProgress.expirationTime = expirationTime; + // Don't touching the subtree's expiration time, which has not changed. + workInProgress.childExpirationTime = current.childExpirationTime; + if (pendingProps !== current.pendingProps) { + // This fiber has new props. + workInProgress.expirationTime = expirationTime; + } else { + // This fiber's props have not changed. + workInProgress.expirationTime = current.expirationTime; + } workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; @@ -572,6 +584,7 @@ export function assignFiberPropertiesInDEV( target.firstEffect = source.firstEffect; target.lastEffect = source.lastEffect; target.expirationTime = source.expirationTime; + target.childExpirationTime = source.childExpirationTime; target.alternate = source.alternate; if (enableProfilerTimer) { target.actualDuration = source.actualDuration; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 1532815d87521..2f64fb614667b 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -112,17 +112,7 @@ if (__DEV__) { didWarnAboutStatelessRefs = {}; } -// TODO: Remove this and use reconcileChildrenAtExpirationTime directly. -function reconcileChildren(current, workInProgress, nextChildren) { - reconcileChildrenAtExpirationTime( - current, - workInProgress, - nextChildren, - workInProgress.expirationTime, - ); -} - -export function reconcileChildrenAtExpirationTime( +export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, @@ -155,7 +145,7 @@ export function reconcileChildrenAtExpirationTime( } } -function updateForwardRef(current, workInProgress) { +function updateForwardRef(current, workInProgress, renderExpirationTime) { const render = workInProgress.type.render; const nextProps = workInProgress.pendingProps; const ref = workInProgress.ref; @@ -165,7 +155,11 @@ function updateForwardRef(current, workInProgress) { } else if (workInProgress.memoizedProps === nextProps) { const currentRef = current !== null ? current.ref : null; if (ref === currentRef) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } } @@ -179,50 +173,52 @@ function updateForwardRef(current, workInProgress) { nextChildren = render(nextProps, ref); } - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextProps); return workInProgress.child; } -function updateFragment(current, workInProgress) { +function updateFragment(current, workInProgress, renderExpirationTime) { const nextChildren = workInProgress.pendingProps; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextChildren) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextChildren); return workInProgress.child; } -function updateMode(current, workInProgress) { +function updateMode(current, workInProgress, renderExpirationTime) { const nextChildren = workInProgress.pendingProps.children; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if ( - nextChildren === null || - workInProgress.memoizedProps === nextChildren - ) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextChildren); return workInProgress.child; } -function updateProfiler(current, workInProgress) { - const nextProps = workInProgress.pendingProps; +function updateProfiler(current, workInProgress, renderExpirationTime) { if (enableProfilerTimer) { workInProgress.effectTag |= Update; } - if (workInProgress.memoizedProps === nextProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } + const nextProps = workInProgress.pendingProps; const nextChildren = nextProps.children; - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextProps); return workInProgress.child; } @@ -245,25 +241,11 @@ function updateFunctionalComponent( ) { const fn = workInProgress.type; const nextProps = workInProgress.pendingProps; - - const hasPendingContext = prepareToReadContext( - workInProgress, - renderExpirationTime, - ); - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextProps && !hasPendingContext) { - // TODO: consider bringing fn.shouldComponentUpdate() back. - // It used to be here. - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - const unmaskedContext = getUnmaskedContext(workInProgress); const context = getMaskedContext(workInProgress, unmaskedContext); let nextChildren; - + prepareToReadContext(workInProgress, renderExpirationTime); if (__DEV__) { ReactCurrentOwner.current = workInProgress; ReactCurrentFiber.setCurrentPhase('render'); @@ -275,7 +257,12 @@ function updateFunctionalComponent( // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextProps); return workInProgress.child; } @@ -349,7 +336,11 @@ function finishClassComponent( invalidateContextProvider(workInProgress, false); } - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } const ctor = workInProgress.type; @@ -395,18 +386,13 @@ function finishClassComponent( if (current !== null && didCaptureError) { // If we're recovering from an error, reconcile twice: first to delete // all the existing children. - reconcileChildrenAtExpirationTime( - current, - workInProgress, - null, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, null, renderExpirationTime); workInProgress.child = null; // Now we can continue reconciling like normal. This has the effect of // remounting all children regardless of whether their their // identity matches. } - reconcileChildrenAtExpirationTime( + reconcileChildren( current, workInProgress, nextChildren, @@ -442,66 +428,75 @@ function pushHostRootContext(workInProgress) { function updateHostRoot(current, workInProgress, renderExpirationTime) { pushHostRootContext(workInProgress); - let updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - const nextProps = workInProgress.pendingProps; - const prevState = workInProgress.memoizedState; - const prevChildren = prevState !== null ? prevState.element : null; - processUpdateQueue( + const updateQueue = workInProgress.updateQueue; + invariant( + updateQueue !== null, + 'If the root does not have an updateQueue, we should have already ' + + 'bailed out. This error is likely caused by a bug in React. Please ' + + 'file an issue.', + ); + const nextProps = workInProgress.pendingProps; + const prevState = workInProgress.memoizedState; + const prevChildren = prevState !== null ? prevState.element : null; + processUpdateQueue( + workInProgress, + updateQueue, + nextProps, + null, + renderExpirationTime, + ); + const nextState = workInProgress.memoizedState; + // Caution: React DevTools currently depends on this property + // being called "element". + const nextChildren = nextState.element; + if (nextChildren === prevChildren) { + // If the state is the same as before, that's a bailout because we had + // no work that expires at this time. + resetHydrationState(); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + const root: FiberRoot = workInProgress.stateNode; + if ( + (current === null || current.child === null) && + root.hydrate && + enterHydrationState(workInProgress) + ) { + // If we don't have any current children this might be the first pass. + // We always try to hydrate. If this isn't a hydration pass there won't + // be any children to hydrate which is effectively the same thing as + // not hydrating. + + // This is a bit of a hack. We track the host root as a placement to + // know that we're currently in a mounting state. That way isMounted + // works as expected. We must reset this before committing. + // TODO: Delete this when we delete isMounted and findDOMNode. + workInProgress.effectTag |= Placement; + + // Ensure that children mount into this root without tracking + // side-effects. This ensures that we don't store Placement effects on + // nodes that will be hydrated. + workInProgress.child = mountChildFibers( workInProgress, - updateQueue, - nextProps, null, + nextChildren, renderExpirationTime, ); - const nextState = workInProgress.memoizedState; - // Caution: React DevTools currently depends on this property - // being called "element". - const nextChildren = nextState.element; - - if (nextChildren === prevChildren) { - // If the state is the same as before, that's a bailout because we had - // no work that expires at this time. - resetHydrationState(); - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - const root: FiberRoot = workInProgress.stateNode; - if ( - (current === null || current.child === null) && - root.hydrate && - enterHydrationState(workInProgress) - ) { - // If we don't have any current children this might be the first pass. - // We always try to hydrate. If this isn't a hydration pass there won't - // be any children to hydrate which is effectively the same thing as - // not hydrating. - - // This is a bit of a hack. We track the host root as a placement to - // know that we're currently in a mounting state. That way isMounted - // works as expected. We must reset this before committing. - // TODO: Delete this when we delete isMounted and findDOMNode. - workInProgress.effectTag |= Placement; - - // Ensure that children mount into this root without tracking - // side-effects. This ensures that we don't store Placement effects on - // nodes that will be hydrated. - workInProgress.child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderExpirationTime, - ); - } else { - // Otherwise reset hydration state in case we aborted and resumed another - // root. - resetHydrationState(); - reconcileChildren(current, workInProgress, nextChildren); - } - return workInProgress.child; + } else { + // Otherwise reset hydration state in case we aborted and resumed another + // root. + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + resetHydrationState(); } - resetHydrationState(); - // If there is no update queue, that's a bailout because the root has no props. - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return workInProgress.child; } function updateHostComponent(current, workInProgress, renderExpirationTime) { @@ -512,28 +507,9 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) { } const type = workInProgress.type; - const memoizedProps = workInProgress.memoizedProps; const nextProps = workInProgress.pendingProps; const prevProps = current !== null ? current.memoizedProps : null; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (memoizedProps === nextProps) { - const isHidden = - workInProgress.mode & AsyncMode && - shouldDeprioritizeSubtree(type, nextProps); - if (isHidden) { - // Before bailing out, make sure we've deprioritized a hidden component. - workInProgress.expirationTime = Never; - } - if (!isHidden || renderExpirationTime !== Never) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - // If we're rendering a hidden node at hidden priority, don't bailout. The - // parent is complete, but the children may not be. - } - let nextChildren = nextProps.children; const isDirectTextChild = shouldSetTextContent(type, nextProps); @@ -543,7 +519,7 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) { // this in the host environment that also have access to this prop. That // avoids allocating another HostText fiber and traversing it. nextChildren = null; - } else if (prevProps && shouldSetTextContent(type, prevProps)) { + } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) { // If we're switching from a direct text child to a normal child, or to // empty, we need to schedule the text content to be reset. workInProgress.effectTag |= ContentReset; @@ -557,14 +533,18 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) { workInProgress.mode & AsyncMode && shouldDeprioritizeSubtree(type, nextProps) ) { - // Down-prioritize the children. + // Schedule this fiber to re-render at offscreen priority. Then bailout. workInProgress.expirationTime = Never; - // Bailout and come back to this fiber later. workInProgress.memoizedProps = nextProps; return null; } - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextProps); return workInProgress.child; } @@ -714,7 +694,7 @@ function mountIndeterminateComponent( } } } - reconcileChildren(current, workInProgress, value); + reconcileChildren(current, workInProgress, value, renderExpirationTime); memoizeProps(workInProgress, props); return workInProgress.child; } @@ -727,9 +707,6 @@ function updatePlaceholderComponent( ) { if (enableSuspense) { const nextProps = workInProgress.pendingProps; - const prevProps = workInProgress.memoizedProps; - - const prevDidTimeout = workInProgress.memoizedState === true; // Check if we already attempted to render the normal state. If we did, // and we timed out, render the placeholder state. @@ -744,12 +721,7 @@ function updatePlaceholderComponent( nextDidTimeout = true; // If we're recovering from an error, reconcile twice: first to delete // all the existing children. - reconcileChildrenAtExpirationTime( - current, - workInProgress, - null, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, null, renderExpirationTime); current.child = null; // Now we can continue reconciling like normal. This has the effect of // remounting all children regardless of whether their their @@ -758,13 +730,6 @@ function updatePlaceholderComponent( nextDidTimeout = !alreadyCaptured; } - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (nextProps === prevProps && nextDidTimeout === prevDidTimeout) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - if ((workInProgress.mode & StrictMode) !== NoEffect) { if (nextDidTimeout) { // If the timed-out view commits, schedule an update effect to record @@ -789,7 +754,12 @@ function updatePlaceholderComponent( workInProgress.memoizedProps = nextProps; workInProgress.memoizedState = nextDidTimeout; - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); return workInProgress.child; } else { return null; @@ -799,13 +769,6 @@ function updatePlaceholderComponent( function updatePortalComponent(current, workInProgress, renderExpirationTime) { pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); const nextChildren = workInProgress.pendingProps; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextChildren) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - if (current === null) { // Portals are special because we don't append the children during mount // but at commit. Therefore we need to track insertions which the normal @@ -820,7 +783,12 @@ function updatePortalComponent(current, workInProgress, renderExpirationTime) { ); memoizeProps(workInProgress, nextChildren); } else { - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextChildren); } return workInProgress.child; @@ -832,17 +800,6 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - let canBailOnProps = true; - - if (hasLegacyContextChanged()) { - canBailOnProps = false; - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (oldProps === newProps) { - workInProgress.stateNode = 0; - pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } const newValue = newProps.value; workInProgress.memoizedProps = newProps; @@ -868,10 +825,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { } else { if (oldProps.value === newProps.value) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children && canBailOnProps) { + if ( + oldProps.children === newProps.children && + !hasLegacyContextChanged() + ) { workInProgress.stateNode = 0; pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } changedBits = 0; } else { @@ -885,10 +849,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare ) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children && canBailOnProps) { + if ( + oldProps.children === newProps.children && + !hasLegacyContextChanged() + ) { workInProgress.stateNode = 0; pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } changedBits = 0; } else { @@ -908,10 +879,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { if (changedBits === 0) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children && canBailOnProps) { + if ( + oldProps.children === newProps.children && + !hasLegacyContextChanged() + ) { workInProgress.stateNode = 0; pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } } else { propagateContextChange( @@ -929,28 +907,13 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { pushProvider(workInProgress); const newChildren = newProps.children; - reconcileChildren(current, workInProgress, newChildren); + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); return workInProgress.child; } function updateContextConsumer(current, workInProgress, renderExpirationTime) { const context: ReactContext = workInProgress.type; const newProps = workInProgress.pendingProps; - const oldProps = workInProgress.memoizedProps; - - const hasPendingContext = prepareToReadContext( - workInProgress, - renderExpirationTime, - ); - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (oldProps === newProps && !hasPendingContext) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - - workInProgress.memoizedProps = newProps; - const render = newProps.children; if (__DEV__) { @@ -963,6 +926,7 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { ); } + prepareToReadContext(workInProgress, renderExpirationTime); const newValue = readContext(context, newProps.unstable_observedBits); let newChildren; if (__DEV__) { @@ -976,7 +940,8 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; - reconcileChildren(current, workInProgress, newChildren); + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); + workInProgress.memoizedProps = newProps; return workInProgress.child; } @@ -1000,12 +965,14 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { */ function bailoutOnAlreadyFinishedWork( - current, + current: Fiber | null, workInProgress: Fiber, + renderExpirationTime: ExpirationTime, ): Fiber | null { cancelWorkTimer(workInProgress); if (current !== null) { + // Reuse previous context list workInProgress.firstContextDependency = current.firstContextDependency; } @@ -1014,51 +981,22 @@ function bailoutOnAlreadyFinishedWork( stopBaseRenderTimerIfRunning(); } - // TODO: We should ideally be able to bail out early if the children have no - // more work to do. However, since we don't have a separation of this - // Fiber's priority and its children yet - we don't know without doing lots - // of the same work we do anyway. Once we have that separation we can just - // bail out here if the children has no more work at this priority level. - // if (workInProgress.priorityOfChildren <= priorityLevel) { - // // If there are side-effects in these children that have not yet been - // // committed we need to ensure that they get properly transferred up. - // if (current && current.child !== workInProgress.child) { - // reuseChildrenEffects(workInProgress, child); - // } - // return null; - // } - - cloneChildFibers(current, workInProgress); - return workInProgress.child; -} - -function bailoutOnLowPriority(current, workInProgress) { - cancelWorkTimer(workInProgress); - - if (enableProfilerTimer) { - // Don't update "base" render times for bailouts. - stopBaseRenderTimerIfRunning(); - } - - // TODO: Handle HostComponent tags here as well and call pushHostContext()? - // See PR 8590 discussion for context - switch (workInProgress.tag) { - case HostRoot: - pushHostRootContext(workInProgress); - break; - case ClassComponent: - pushLegacyContextProvider(workInProgress); - break; - case HostPortal: - pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); - break; - case ContextProvider: - pushProvider(workInProgress); - break; + // Check if the children have any pending work. + const childExpirationTime = workInProgress.childExpirationTime; + if ( + childExpirationTime === NoWork || + childExpirationTime > renderExpirationTime + ) { + // The children don't have any work either. We can skip them. + // TODO: Once we add back resuming, we should check if the children are + // a work-in-progress set. If so, we need to transfer their effects. + return null; + } else { + // This fiber doesn't have work, but its subtree does. Clone the child + // fibers and continue. + cloneChildFibers(current, workInProgress); + return workInProgress.child; } - // TODO: What if this is currently in progress? - // How can that happen? How is this not being cloned? - return null; } // TODO: Delete memoizeProps/State and move to reconcile/bailout instead @@ -1083,13 +1021,52 @@ function beginWork( } } + const updateExpirationTime = workInProgress.expirationTime; if ( - workInProgress.expirationTime === NoWork || - workInProgress.expirationTime > renderExpirationTime + !hasLegacyContextChanged() && + (updateExpirationTime === NoWork || + updateExpirationTime > renderExpirationTime) ) { - return bailoutOnLowPriority(current, workInProgress); + // This fiber does not have any pending work. Bailout without entering + // the begin phase. There's still some bookkeeping we that needs to be done + // in this optimized path, mostly pushing stuff onto the stack. + switch (workInProgress.tag) { + case HostRoot: + pushHostRootContext(workInProgress); + resetHydrationState(); + break; + case HostComponent: + pushHostContext(workInProgress); + break; + case ClassComponent: + pushLegacyContextProvider(workInProgress); + break; + case HostPortal: + pushHostContainer( + workInProgress, + workInProgress.stateNode.containerInfo, + ); + break; + case ContextProvider: + workInProgress.stateNode = 0; + pushProvider(workInProgress); + break; + case Profiler: + if (enableProfilerTimer) { + workInProgress.effectTag |= Update; + } + break; + } + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } + // Before entering the begin phase, clear the expiration time. + workInProgress.expirationTime = NoWork; + switch (workInProgress.tag) { case IndeterminateComponent: return mountIndeterminateComponent( @@ -1128,13 +1105,13 @@ function beginWork( renderExpirationTime, ); case ForwardRef: - return updateForwardRef(current, workInProgress); + return updateForwardRef(current, workInProgress, renderExpirationTime); case Fragment: - return updateFragment(current, workInProgress); + return updateFragment(current, workInProgress, renderExpirationTime); case Mode: - return updateMode(current, workInProgress); + return updateMode(current, workInProgress, renderExpirationTime); case Profiler: - return updateProfiler(current, workInProgress); + return updateProfiler(current, workInProgress, renderExpirationTime); case ContextProvider: return updateContextProvider( current, diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 6ef80abb42af5..cc2103fe22f8e 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -162,7 +162,7 @@ export function applyDerivedStateFromProps( // Once the update queue is empty, persist the derived state onto the // base state. const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.expirationTime === NoWork) { + if (updateQueue !== null && workInProgress.expirationTime === NoWork) { updateQueue.baseState = memoizedState; } } @@ -183,7 +183,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, enqueueReplaceState(inst, payload, callback) { @@ -202,7 +202,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, enqueueForceUpdate(inst, callback) { @@ -220,7 +220,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, }; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 92035588440e4..39d51c642ce07 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -118,29 +118,44 @@ export function propagateContextChange( dependency.context === context && (dependency.observedBits & changedBits) !== 0 ) { - // Match! Update the expiration time of all the ancestors, including + // Match! Schedule an update on this fiber. + if ( + fiber.expirationTime === NoWork || + fiber.expirationTime > renderExpirationTime + ) { + fiber.expirationTime = renderExpirationTime; + } + let alternate = fiber.alternate; + if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > renderExpirationTime) + ) { + alternate.expirationTime = renderExpirationTime; + } + // Update the child expiration time of all the ancestors, including // the alternates. - let node = fiber; + let node = fiber.return; while (node !== null) { - const alternate = node.alternate; + alternate = node.alternate; if ( - node.expirationTime === NoWork || - node.expirationTime > renderExpirationTime + node.childExpirationTime === NoWork || + node.childExpirationTime > renderExpirationTime ) { - node.expirationTime = renderExpirationTime; + node.childExpirationTime = renderExpirationTime; if ( alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > renderExpirationTime) + (alternate.childExpirationTime === NoWork || + alternate.childExpirationTime > renderExpirationTime) ) { - alternate.expirationTime = renderExpirationTime; + alternate.childExpirationTime = renderExpirationTime; } } else if ( alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > renderExpirationTime) + (alternate.childExpirationTime === NoWork || + alternate.childExpirationTime > renderExpirationTime) ) { - alternate.expirationTime = renderExpirationTime; + alternate.childExpirationTime = renderExpirationTime; } else { // Neither alternate was updated, which means the rest of the // ancestor path already has sufficient priority. diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index ce9e54c972371..140b75e3aa70c 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -135,7 +135,7 @@ function scheduleRootUpdate( ); update.callback = callback; } - enqueueUpdate(current, update, expirationTime); + enqueueUpdate(current, update); scheduleWork(current, expirationTime); return expirationTime; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 0c0004fc916d6..014b1f6e184ae 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -533,8 +533,15 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { // Update the pending priority levels to account for the work that we are // about to commit. This needs to happen before calling the lifecycles, since // they may schedule additional updates. - const earliestRemainingTime = finishedWork.expirationTime; - markCommittedPriorityLevels(root, earliestRemainingTime); + const updateExpirationTimeBeforeCommit = finishedWork.expirationTime; + const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime; + const earliestRemainingTimeBeforeCommit = + updateExpirationTimeBeforeCommit === NoWork || + (childExpirationTimeBeforeCommit !== NoWork && + childExpirationTimeBeforeCommit < updateExpirationTimeBeforeCommit) + ? childExpirationTimeBeforeCommit + : updateExpirationTimeBeforeCommit; + markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit); // Reset this to null before calling lifecycles ReactCurrentOwner.current = null; @@ -706,71 +713,85 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork); } - const expirationTime = root.expirationTime; - if (expirationTime === NoWork) { + const updateExpirationTimeAfterCommit = finishedWork.expirationTime; + const childExpirationTimeAfterCommit = finishedWork.childExpirationTime; + const earliestRemainingTimeAfterCommit = + updateExpirationTimeAfterCommit === NoWork || + (childExpirationTimeAfterCommit !== NoWork && + childExpirationTimeAfterCommit < updateExpirationTimeAfterCommit) + ? childExpirationTimeAfterCommit + : updateExpirationTimeAfterCommit; + if (earliestRemainingTimeAfterCommit === NoWork) { // If there's no remaining work, we can clear the set of already failed // error boundaries. legacyErrorBoundariesThatAlreadyFailed = null; } - onCommit(root, expirationTime); + onCommit(root, earliestRemainingTimeAfterCommit); } -function resetExpirationTime( +function resetChildExpirationTime( workInProgress: Fiber, renderTime: ExpirationTime, ) { - if (renderTime !== Never && workInProgress.expirationTime === Never) { + if (renderTime !== Never && workInProgress.childExpirationTime === Never) { // The children of this component are hidden. Don't bubble their // expiration times. return; } - // Check for pending updates. - let newExpirationTime = NoWork; - switch (workInProgress.tag) { - case HostRoot: - case ClassComponent: { - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - newExpirationTime = updateQueue.expirationTime; - } - } - } - - // TODO: Calls need to visit stateNode + let newChildExpirationTime = NoWork; // Bubble up the earliest expiration time. - // (And "base" render timers if that feature flag is enabled) if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // We're in profiling mode. Let's use this same traversal to update the + // "base" render times. let treeBaseDuration = workInProgress.selfBaseDuration; let child = workInProgress.child; while (child !== null) { - treeBaseDuration += child.treeBaseDuration; + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; if ( - child.expirationTime !== NoWork && - (newExpirationTime === NoWork || - newExpirationTime > child.expirationTime) + newChildExpirationTime === NoWork || + (childUpdateExpirationTime !== NoWork && + childUpdateExpirationTime < newChildExpirationTime) ) { - newExpirationTime = child.expirationTime; + newChildExpirationTime = childUpdateExpirationTime; } + if ( + newChildExpirationTime === NoWork || + (childChildExpirationTime !== NoWork && + childChildExpirationTime < newChildExpirationTime) + ) { + newChildExpirationTime = childChildExpirationTime; + } + treeBaseDuration += child.treeBaseDuration; child = child.sibling; } workInProgress.treeBaseDuration = treeBaseDuration; } else { let child = workInProgress.child; while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; if ( - child.expirationTime !== NoWork && - (newExpirationTime === NoWork || - newExpirationTime > child.expirationTime) + newChildExpirationTime === NoWork || + (childUpdateExpirationTime !== NoWork && + childUpdateExpirationTime < newChildExpirationTime) ) { - newExpirationTime = child.expirationTime; + newChildExpirationTime = childUpdateExpirationTime; + } + if ( + newChildExpirationTime === NoWork || + (childChildExpirationTime !== NoWork && + childChildExpirationTime < newChildExpirationTime) + ) { + newChildExpirationTime = childChildExpirationTime; } child = child.sibling; } } - workInProgress.expirationTime = newExpirationTime; + workInProgress.childExpirationTime = newChildExpirationTime; } function completeUnitOfWork(workInProgress: Fiber): Fiber | null { @@ -799,7 +820,7 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null { ); let next = nextUnitOfWork; stopWorkTimer(workInProgress); - resetExpirationTime(workInProgress, nextRenderExpirationTime); + resetChildExpirationTime(workInProgress, nextRenderExpirationTime); if (__DEV__) { ReactCurrentFiber.resetCurrentFiber(); } @@ -1270,7 +1291,7 @@ function dispatch( errorInfo, expirationTime, ); - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); return; } @@ -1278,7 +1299,7 @@ function dispatch( case HostRoot: { const errorInfo = createCapturedValue(value, sourceFiber); const update = createRootErrorUpdate(fiber, errorInfo, expirationTime); - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); return; } @@ -1292,7 +1313,7 @@ function dispatch( const rootFiber = sourceFiber; const errorInfo = createCapturedValue(value, rootFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, expirationTime); - enqueueUpdate(rootFiber, update, expirationTime); + enqueueUpdate(rootFiber, update); scheduleWork(rootFiber, expirationTime); } } @@ -1411,35 +1432,52 @@ function retrySuspendedRoot( } function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null { - // Walk the parent path to the root and update each node's - // expiration time. - let node = fiber; - do { - const alternate = node.alternate; + // Update the source fiber's expiration time + if ( + fiber.expirationTime === NoWork || + fiber.expirationTime > expirationTime + ) { + fiber.expirationTime = expirationTime; + } + let alternate = fiber.alternate; + if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > expirationTime) + ) { + alternate.expirationTime = expirationTime; + } + // Walk the parent path to the root and update the child expiration time. + let node = fiber.return; + if (node === null && fiber.tag === HostRoot) { + return fiber.stateNode; + } + while (node !== null) { + alternate = node.alternate; if ( - node.expirationTime === NoWork || - node.expirationTime > expirationTime + node.childExpirationTime === NoWork || + node.childExpirationTime > expirationTime ) { - node.expirationTime = expirationTime; + node.childExpirationTime = expirationTime; if ( alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > expirationTime) + (alternate.childExpirationTime === NoWork || + alternate.childExpirationTime > expirationTime) ) { - alternate.expirationTime = expirationTime; + alternate.childExpirationTime = expirationTime; } } else if ( alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > expirationTime) + (alternate.childExpirationTime === NoWork || + alternate.childExpirationTime > expirationTime) ) { - alternate.expirationTime = expirationTime; + alternate.childExpirationTime = expirationTime; } if (node.return === null && node.tag === HostRoot) { return node.stateNode; } node = node.return; - } while (node !== null); + } return null; } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index f60e3147933a7..489c669473716 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -70,7 +70,7 @@ import { LOW_PRIORITY_EXPIRATION, } from './ReactFiberExpirationTime'; import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority'; -import {reconcileChildrenAtExpirationTime} from './ReactFiberBeginWork'; +import {reconcileChildren} from './ReactFiberBeginWork'; function NoopComponent() { return null; @@ -238,7 +238,7 @@ function throwException( // Unmount the source fiber's children const nextChildren = null; - reconcileChildrenAtExpirationTime( + reconcileChildren( sourceFiber.alternate, sourceFiber, nextChildren, @@ -310,6 +310,7 @@ function throwException( renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime); workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; return; } // This boundary already captured during this render. Continue to the @@ -334,12 +335,13 @@ function throwException( case HostRoot: { const errorInfo = value; workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; const update = createRootErrorUpdate( workInProgress, errorInfo, renderExpirationTime, ); - enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); + enqueueCapturedUpdate(workInProgress, update); return; } case ClassComponent: @@ -356,13 +358,14 @@ function throwException( !isAlreadyFailedLegacyErrorBoundary(instance))) ) { workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; // Schedule the error boundary to re-render using updated state const update = createClassErrorUpdate( workInProgress, errorInfo, renderExpirationTime, ); - enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); + enqueueCapturedUpdate(workInProgress, update); return; } break; diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index f592d9e54fc51..22e88ae8cc484 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -117,7 +117,6 @@ export type Update = { }; export type UpdateQueue = { - expirationTime: ExpirationTime, baseState: State, firstUpdate: Update | null, @@ -156,7 +155,6 @@ if (__DEV__) { export function createUpdateQueue(baseState: State): UpdateQueue { const queue: UpdateQueue = { - expirationTime: NoWork, baseState, firstUpdate: null, lastUpdate: null, @@ -174,7 +172,6 @@ function cloneUpdateQueue( currentQueue: UpdateQueue, ): UpdateQueue { const queue: UpdateQueue = { - expirationTime: currentQueue.expirationTime, baseState: currentQueue.baseState, firstUpdate: currentQueue.firstUpdate, lastUpdate: currentQueue.lastUpdate, @@ -209,7 +206,6 @@ export function createUpdate(expirationTime: ExpirationTime): Update<*> { function appendUpdateToQueue( queue: UpdateQueue, update: Update, - expirationTime: ExpirationTime, ) { // Append the update to the end of the list. if (queue.lastUpdate === null) { @@ -219,21 +215,9 @@ function appendUpdateToQueue( queue.lastUpdate.next = update; queue.lastUpdate = update; } - if ( - queue.expirationTime === NoWork || - queue.expirationTime > expirationTime - ) { - // The incoming update has the earliest expiration of any update in the - // queue. Update the queue's expiration time. - queue.expirationTime = expirationTime; - } } -export function enqueueUpdate( - fiber: Fiber, - update: Update, - expirationTime: ExpirationTime, -) { +export function enqueueUpdate(fiber: Fiber, update: Update) { // Update queues are created lazily. const alternate = fiber.alternate; let queue1; @@ -271,19 +255,19 @@ export function enqueueUpdate( } if (queue2 === null || queue1 === queue2) { // There's only a single queue. - appendUpdateToQueue(queue1, update, expirationTime); + appendUpdateToQueue(queue1, update); } else { // There are two queues. We need to append the update to both queues, // while accounting for the persistent structure of the list — we don't // want the same update to be added multiple times. if (queue1.lastUpdate === null || queue2.lastUpdate === null) { // One of the queues is not empty. We must add the update to both queues. - appendUpdateToQueue(queue1, update, expirationTime); - appendUpdateToQueue(queue2, update, expirationTime); + appendUpdateToQueue(queue1, update); + appendUpdateToQueue(queue2, update); } else { // Both queues are non-empty. The last update is the same in both lists, // because of structural sharing. So, only append to one of the lists. - appendUpdateToQueue(queue1, update, expirationTime); + appendUpdateToQueue(queue1, update); // But we still need to update the `lastUpdate` pointer of queue2. queue2.lastUpdate = update; } @@ -311,7 +295,6 @@ export function enqueueUpdate( export function enqueueCapturedUpdate( workInProgress: Fiber, update: Update, - renderExpirationTime: ExpirationTime, ) { // Captured updates go into a separate list, and only on the work-in- // progress queue. @@ -338,14 +321,6 @@ export function enqueueCapturedUpdate( workInProgressQueue.lastCapturedUpdate.next = update; workInProgressQueue.lastCapturedUpdate = update; } - if ( - workInProgressQueue.expirationTime === NoWork || - workInProgressQueue.expirationTime > renderExpirationTime - ) { - // The incoming update has the earliest expiration of any update in the - // queue. Update the queue's expiration time. - workInProgressQueue.expirationTime = renderExpirationTime; - } } function ensureWorkInProgressQueueIsAClone( @@ -438,14 +413,6 @@ export function processUpdateQueue( ): void { hasForceUpdate = false; - if ( - queue.expirationTime === NoWork || - queue.expirationTime > renderExpirationTime - ) { - // Insufficient priority. Bailout. - return; - } - queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue); if (__DEV__) { @@ -577,8 +544,15 @@ export function processUpdateQueue( queue.baseState = newBaseState; queue.firstUpdate = newFirstUpdate; queue.firstCapturedUpdate = newFirstCapturedUpdate; - queue.expirationTime = newExpirationTime; + // Set the remaining expiration time to be whatever is remaining in the queue. + // This should be fine because the only two other things that contribute to + // expiration time are props and context. We're already in the middle of the + // begin phase by the time we start processing the queue, so we've already + // dealt with the props. Context in components that specify + // shouldComponentUpdate is tricky; but we'll have to account for + // that regardless. + workInProgress.expirationTime = newExpirationTime; workInProgress.memoizedState = resultState; if (__DEV__) { From 47d45203646c16714e80fa200588dc607c6653ea Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 20 Jul 2018 16:36:45 -0700 Subject: [PATCH 5/5] Performance nits Optimize `readContext` for most common case --- .../src/ReactFiberNewContext.js | 47 +++++++++++-------- .../src/ReactFiberScheduler.js | 3 +- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 39d51c642ce07..a267df48bf208 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -38,7 +38,15 @@ if (__DEV__) { let currentlyRenderingFiber: Fiber | null = null; let lastContextDependency: ContextDependency | null = null; -let memoizedFirstContextDependency: ContextDependency | null = null; +let lastContext: ReactContext | null = null; + +export function resetContextDependences(): void { + // This is called right before React yields execution, to ensure `readContext` + // cannot be called outside the render phase. + currentlyRenderingFiber = null; + lastContextDependency = null; + lastContext = null; +} export function pushProvider(providerFiber: Fiber): void { const context: ReactContext = providerFiber.type._context; @@ -212,15 +220,18 @@ export function prepareToReadContext( renderExpirationTime: ExpirationTime, ): boolean { currentlyRenderingFiber = workInProgress; - memoizedFirstContextDependency = workInProgress.firstContextDependency; - if (memoizedFirstContextDependency !== null) { + lastContextDependency = null; + lastContext = null; + + const firstContextDependency = workInProgress.firstContextDependency; + if (firstContextDependency !== null) { // Reset the work-in-progress list workInProgress.firstContextDependency = null; // Iterate through the context dependencies and see if there were any // changes. If so, continue propagating the context change by scanning // the child subtree. - let dependency = memoizedFirstContextDependency; + let dependency = firstContextDependency; let hasPendingContext = false; do { const context = dependency.context; @@ -253,12 +264,6 @@ export function readContext( context: ReactContext, observedBits: void | number | boolean, ): T { - invariant( - currentlyRenderingFiber !== null, - 'Context.unstable_read(): Context can only be read while React is ' + - 'rendering, e.g. inside the render method or getDerivedStateFromProps.', - ); - if (typeof observedBits !== 'number') { if (observedBits === false) { // Do not observe updates @@ -269,30 +274,34 @@ export function readContext( } } - if (currentlyRenderingFiber.firstContextDependency === null) { + if (lastContext === null) { + invariant( + currentlyRenderingFiber !== null, + 'Context.unstable_read(): Context can only be read while React is ' + + 'rendering, e.g. inside the render method or getDerivedStateFromProps.', + ); // This is the first dependency in the list currentlyRenderingFiber.firstContextDependency = lastContextDependency = { context: ((context: any): ReactContext), observedBits, next: null, }; + lastContext = context; } else { - invariant( - lastContextDependency !== null, - 'Expected a non-empty list of context dependencies. This is likely a ' + - 'bug in React. Please file an issue.', - ); - if (lastContextDependency.context === context) { + // `lastContextDependency` is always non-null if `lastContext is. + const lastDependency: ContextDependency = (lastContextDependency: any); + if (lastContext === context) { // Fast path. The previous context has the same type. We can reuse // the same node. - lastContextDependency.observedBits |= observedBits; + lastDependency.observedBits |= observedBits; } else { // Append a new context item. - lastContextDependency = lastContextDependency.next = { + lastContextDependency = lastDependency.next = { context: ((context: any): ReactContext), observedBits, next: null, }; + lastContext = context; } } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 014b1f6e184ae..325522b2c4b4e 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -109,7 +109,7 @@ import { popTopLevelContextObject as popTopLevelLegacyContextObject, popContextProvider as popLegacyContextProvider, } from './ReactFiberContext'; -import {popProvider} from './ReactFiberNewContext'; +import {popProvider, resetContextDependences} from './ReactFiberNewContext'; import {popHostContext, popHostContainer} from './ReactFiberHostContext'; import { checkActualRenderTimeStackEmpty, @@ -1131,6 +1131,7 @@ function renderRoot( // We're done performing work. Time to clean up. isWorking = false; ReactCurrentOwner.currentDispatcher = null; + resetContextDependences(); // Yield back to main thread. if (didFatal) {