From 78be8f626f316589047a6c295e9589d5ac06e532 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 2 Jul 2018 13:19:58 -0700 Subject: [PATCH 1/4] 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 | 146 ++----------- .../src/ReactFiberNewContext.js | 204 ++++++++++++++++-- .../src/ReactFiberUnwindWork.js | 2 +- 4 files changed, 210 insertions(+), 149 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 7e5a9dd3d6194..f65fd107b1cd7 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 {ContextReader} 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 + firstContextReader: ContextReader | 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.firstContextReader = null; this.mode = mode; @@ -331,6 +336,7 @@ export function createWorkInProgress( workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; + workInProgress.firstContextReader = current.firstContextReader; // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; @@ -562,6 +568,7 @@ export function assignFiberPropertiesInDEV( target.memoizedProps = source.memoizedProps; target.updateQueue = source.updateQueue; target.memoizedState = source.memoizedState; + target.firstContextReader = source.firstContextReader; 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 d2573168ec343..805340c8b5f74 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -69,8 +69,11 @@ import { import {pushHostContext, pushHostContainer} from './ReactFiberHostContext'; import { pushProvider, - getContextCurrentValue, - getContextChangedBits, + propagateContextChange, + checkForPendingContext, + readContext, + prepareToReadContext, + finishReadingContext, } from './ReactFiberNewContext'; import { markActualRenderTimeStarted, @@ -764,100 +767,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; - 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 { - // 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; @@ -970,42 +879,16 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - const newValue = getContextCurrentValue(context); - const changedBits = getContextChangedBits(context); - - 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) { - 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; + if (!checkForPendingContext(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) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } } - // 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; @@ -1019,6 +902,8 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { ); } + prepareToReadContext(); + const newValue = readContext(context, newProps.unstable_observedBits); let newChildren; if (__DEV__) { ReactCurrentOwner.current = workInProgress; @@ -1028,6 +913,7 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { } else { newChildren = render(newValue); } + workInProgress.firstContextReader = finishReadingContext(); // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 4826412dded63..02a01c652c451 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -7,20 +7,27 @@ * @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 ContextReader = { + context: ReactContext, + observedBits: number, + next: ContextReader | null, }; +let nextFirstReader: ContextReader | null = null; +let nextLastReader: ContextReader | null = null; + import warning from 'shared/warning'; + import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack'; +import maxSigned31BitInt from './maxSigned31BitInt'; +import {NoWork} from './ReactFiberExpirationTime'; +import {ContextProvider} from 'shared/ReactTypeOfWork'; const providerCursor: StackCursor = createCursor(null); const valueCursor: StackCursor = createCursor(null); @@ -32,7 +39,7 @@ if (__DEV__) { rendererSigil = {}; } -function pushProvider(providerFiber: Fiber): void { +export function pushProvider(providerFiber: Fiber): void { const context: ReactContext = providerFiber.type._context; if (isPrimaryRenderer) { @@ -72,7 +79,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 +97,178 @@ 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 reader = fiber.firstContextReader; + if (reader !== null) { + do { + // Check if the context matches. + if ( + reader.context === context && + (reader.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; + } + reader = reader.next; + } while (reader !== 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; + } +} + +export function checkForPendingContext( + workInProgress: Fiber, + renderExpirationTime: ExpirationTime, +): boolean { + let reader = workInProgress.firstContextReader; + let hasPendingContext = false; + while (reader !== null) { + const context = reader.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 & reader.observedBits) !== 0) { + hasPendingContext = true; + } + } + reader = reader.next; + } + return hasPendingContext; } -function getContextChangedBits(context: ReactContext): number { - return isPrimaryRenderer ? context._changedBits : context._changedBits2; +export function prepareToReadContext(): void { + nextFirstReader = nextLastReader = null; } -export { - pushProvider, - popProvider, - getContextCurrentValue, - getContextChangedBits, -}; +export function readContext( + context: ReactContext, + observedBits: void | number | boolean, +): T { + if (typeof observedBits !== 'number') { + if (observedBits === false) { + // Do not observe updates + observedBits = 0; + } else { + // Observe all updates + observedBits = maxSigned31BitInt; + } + } + + if (nextLastReader !== null) { + if (nextLastReader.context === context) { + // Fast path. The previous context has the same type. We can reuse + // the same node. + nextLastReader.observedBits |= observedBits; + } else { + // Append a new context item. + nextLastReader = nextLastReader.next = { + context: ((context: any): ReactContext), + observedBits, + next: null, + }; + } + } else { + // This is the first reader in the list + nextFirstReader = nextLastReader = { + context: ((context: any): ReactContext), + observedBits, + next: null, + }; + } + + return isPrimaryRenderer ? context._currentValue : context._currentValue2; +} + +export function finishReadingContext(): ContextReader | null { + const list = nextFirstReader; + nextFirstReader = nextLastReader = null; + return list; +} diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 8bc5efa876b5a..4d0c2331c0bbf 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -71,7 +71,7 @@ function createRootErrorUpdate( fiber: Fiber, errorInfo: CapturedValue, expirationTime: ExpirationTime, -): Update { +): Update { const update = createUpdate(expirationTime); // Unmount the root by rendering null. update.tag = CaptureUpdate; From a2a279356df3fb5e69b0fad9c774c5c475addfa0 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 2 Jul 2018 15:19:58 -0700 Subject: [PATCH 2/4] 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 | 41 +- .../src/ReactFiberClassComponent.js | 63 +- .../src/ReactFiberDispatcher.js | 14 + .../src/ReactFiberScheduler.js | 3 + .../ReactNewContext-test.internal.js | 2111 +++++++++-------- packages/react/src/ReactContext.js | 18 + packages/react/src/ReactCurrentOwner.js | 2 + packages/shared/ReactTypes.js | 1 + 8 files changed, 1266 insertions(+), 987 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 805340c8b5f74..d3d479da64eab 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -239,19 +239,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; - 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); + if (!checkForPendingContext(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); + } + // TODO: consider bringing fn.shouldComponentUpdate() back. + // It used to be here. } - // TODO: consider bringing fn.shouldComponentUpdate() back. - // It used to be here. } const unmaskedContext = getUnmaskedContext(workInProgress); @@ -259,6 +265,7 @@ function updateFunctionalComponent(current, workInProgress) { let nextChildren; + prepareToReadContext(); if (__DEV__) { ReactCurrentOwner.current = workInProgress; ReactDebugCurrentFiber.setCurrentPhase('render'); @@ -267,6 +274,8 @@ function updateFunctionalComponent(current, workInProgress) { } else { nextChildren = fn(nextProps, context); } + workInProgress.firstContextReader = finishReadingContext(); + // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren); @@ -283,6 +292,8 @@ 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); + prepareToReadContext(); + let shouldUpdate; if (current === null) { if (workInProgress.stateNode === null) { @@ -377,6 +388,8 @@ function finishClassComponent( } } + workInProgress.firstContextReader = finishReadingContext(); + // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; if (didCaptureError) { @@ -582,6 +595,8 @@ function mountIndeterminateComponent( const unmaskedContext = getUnmaskedContext(workInProgress); const context = getMaskedContext(workInProgress, unmaskedContext); + prepareToReadContext(); + let value; if (__DEV__) { @@ -612,6 +627,8 @@ function mountIndeterminateComponent( // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; + workInProgress.firstContextReader = finishReadingContext(); + if ( typeof value === 'object' && value !== null && @@ -1035,7 +1052,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 a0ae9fb7823f9..60365b56d8fd1 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -50,6 +50,7 @@ import { computeExpirationForFiber, scheduleWork, } from './ReactFiberScheduler'; +import {checkForPendingContext} from './ReactFiberNewContext'; const fakeInternalInstance = {}; const isArray = Array.isArray; @@ -231,7 +232,7 @@ function checkShouldComponentUpdate( newProps, oldState, newState, - newContext, + nextLegacyContext, ) { const instance = workInProgress.stateNode; const ctor = workInProgress.type; @@ -240,7 +241,7 @@ function checkShouldComponentUpdate( const shouldUpdate = instance.shouldComponentUpdate( newProps, newState, - newContext, + nextLegacyContext, ); stopPhaseTimer(); @@ -616,15 +617,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(); @@ -746,8 +747,16 @@ 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 hasPendingNewContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = @@ -765,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, ); } } @@ -794,6 +803,7 @@ function resumeMountClassInstance( oldProps === newProps && oldState === newState && !hasContextChanged() && + !hasPendingNewContext && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update @@ -815,13 +825,14 @@ function resumeMountClassInstance( const shouldUpdate = checkHasForceUpdateAfterProcessing() || + hasPendingNewContext || checkShouldComponentUpdate( workInProgress, oldProps, newProps, oldState, newState, - newContext, + nextLegacyContext, ); if (shouldUpdate) { @@ -861,7 +872,7 @@ function resumeMountClassInstance( // if shouldComponentUpdate returns false. instance.props = newProps; instance.state = newState; - instance.context = newContext; + instance.context = nextLegacyContext; return shouldUpdate; } @@ -880,8 +891,16 @@ 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 hasPendingNewContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = @@ -899,12 +918,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, ); } } @@ -929,6 +948,7 @@ function updateClassInstance( oldProps === newProps && oldState === newState && !hasContextChanged() && + !hasPendingNewContext && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update @@ -963,13 +983,14 @@ function updateClassInstance( const shouldUpdate = checkHasForceUpdateAfterProcessing() || + hasPendingNewContext || checkShouldComponentUpdate( workInProgress, oldProps, newProps, oldState, newState, - newContext, + nextLegacyContext, ); if (shouldUpdate) { @@ -982,10 +1003,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(); } @@ -1025,7 +1050,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 25a90af822eff..5f82daa6a1ec7 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -135,6 +135,7 @@ import { commitAttachRef, commitDetachRef, } from './ReactFiberCommitWork'; +import {Dispatcher} from './ReactFiberDispatcher'; export type Deadline = { timeRemaining: () => number, @@ -985,6 +986,7 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void { 'by a bug in React. Please file an issue.', ); isWorking = true; + ReactCurrentOwner.currentDispatcher = Dispatcher; const expirationTime = root.nextExpirationTimeToWorkOn; @@ -1071,6 +1073,7 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void { // 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 95355e2a1864d..e090239540bd6 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -30,1105 +30,1300 @@ 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'}); - - const Verify = ({actual, expected}) => { - expect(expected).toBe(actual); - return null; - }; - - ReactNoop.render( - - - - {({value}) => } - - - - - {({value}) => } - - - - - - {({value}) => } - - - {({value}) => } - - , - ); - ReactNoop.flush(); - }); + 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; + }; + + 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} + + )} + + ); + } + + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } + + function App(props) { + return ( + + + + + + {value => } + + + + + + {value => } + + + + + ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([ + span('Result: 4'), + span('Result: 2'), + ]); - it('multiple consumers in different branches', () => { - const Context = React.createContext(1); + // Update + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([ + span('Result: 6'), + span('Result: 3'), + ]); - function Provider(props) { - return ( - - {contextValue => ( - // Multiply previous context value by 2, unless prop overrides - + // 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} - )} - - ); - } - - function Consumer(props) { - return ( - - {value => } - - ); - } + ); + } - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - return this.props.children; - } - } + function Consumer(props) { + ReactNoop.yield('Consumer'); + return ( + + {value => { + ReactNoop.yield('Consumer render prop'); + return ; + }} + + ); + } - function App(props) { - return ( - - - - - - - - - - - - - ); - } + 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: 4'), - span('Result: 2'), - ]); - - // Update - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([ - span('Result: 6'), - span('Result: 3'), - ]); - - // 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', () => { - spyOnDev(console, 'error'); + // 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); - const Context = React.createContext( - 0, - (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int - ); + class App extends React.Component { + state = { + step: 0, + }; - ReactNoop.render(); - ReactNoop.flush(); + render() { + ReactNoop.yield('App'); + return ( + + + {this.state.step > 0 && } + + ); + } + } - // Update - ReactNoop.render(); - ReactNoop.flush(); + class StaticContent extends React.PureComponent { + render() { + return ( + + + + + + + ); + } + } - if (__DEV__) { - expect(console.error).toHaveBeenCalledTimes(1); - expect(console.error.calls.argsFor(0)[0]).toContain( - 'calculateChangedBits: Expected the return value to be a 31-bit ' + - 'integer. Instead received: 4294967295', - ); - } - }); + class Indirection extends React.PureComponent { + render() { + return ( + + {value => { + ReactNoop.yield('Consumer'); + return ; + }} + + ); + } + } - it('warns if multiple renderers concurrently render the same context', () => { - spyOnDev(console, 'error'); - const Context = React.createContext(0); + // 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 Foo(props) { - ReactNoop.yield('Foo'); - return null; - } + describe('Context.Provider', () => { + it('warns if calculateChangedBits returns larger than a 31-bit integer', () => { + spyOnDev(console, 'error'); - function App(props) { - return ( - - - - + 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']); + ReactNoop.render(); + ReactNoop.flush(); - // Get a new copy of ReactNoop - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - React = require('react'); - ReactNoop = require('react-noop-renderer'); + // Update + ReactNoop.render(); + ReactNoop.flush(); - // Render the provider again using a different renderer - 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', - ); - } - }); + if (__DEV__) { + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + '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} />; - }} - - - ); - } + ReactNoop.render(); + // Render past the Provider, but don't commit yet + ReactNoop.flushThrough(['Foo']); - // Initial mount - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']); - expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]); + // Get a new copy of ReactNoop + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + React = require('react'); + ReactNoop = require('react-noop-renderer'); - 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} - ); - } + // Render the provider again using a different renderer + ReactNoop.render(); + ReactNoop.flush(); - // 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')]); - }); - - it('provider does not bail out if legacy context changed above', () => { - const Context = React.createContext(0); - - 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')]); - }); + // 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')]); + }); - it('consumer bails out if value is unchanged and something above bailed out', () => { - const Context = React.createContext(0); + it('provider does not bail out if legacy context changed above', () => { + const Context = React.createContext(0); - function renderChildValue(value) { - ReactNoop.yield('Consumer'); - return ; - } + function Child() { + ReactNoop.yield('Child'); + return ; + } - function ChildWithInlineRenderCallback() { - ReactNoop.yield('ChildWithInlineRenderCallback'); - // Note: we are intentionally passing an inline arrow. Don't refactor. - return ( - {value => renderChildValue(value)} - ); - } + const children = ; - function ChildWithCachedRenderCallback() { - ReactNoop.yield('ChildWithCachedRenderCallback'); - return {renderChildValue}; - } - - 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)]); - }); + const legacyProviderRef = React.createRef(); + const appRef = React.createRef(); - // 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); - - 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; + } + } + + function App(props) { + return ( + + + + + + + + + + + ); + } - // 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), - ]); + 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'), + ]); + }); }); describe('fuzz test', () => { diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 4c1b06bfed5d3..8a972139b6e36 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 warning from 'shared/warning'; +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 18d8ea5a2e81ebbb9520ae3280dc1a9932bf557d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 2 Jul 2018 22:33:32 -0700 Subject: [PATCH 3/4] Unify context implementations Implements the legacy, string-based context API on top of the new context API. This doesn't save much in code size because most of the legacy stuff deals with merging and masking. Arguably, it's a conceptual complexity win. I'm ambivalent about whether we land this. --- .../src/ReactFiberBeginWork.js | 312 ++++++++++------ .../src/ReactFiberClassComponent.js | 53 +-- .../src/ReactFiberCompleteWork.js | 9 +- .../react-reconciler/src/ReactFiberContext.js | 340 +++++++----------- .../src/ReactFiberNewContext.js | 51 ++- .../src/ReactFiberReconciler.js | 17 +- .../src/ReactFiberScheduler.js | 11 +- .../src/ReactFiberUnwindWork.js | 16 +- ...ernal.js => ReactContext-test.internal.js} | 2 +- packages/shared/ReactTypeOfSideEffect.js | 29 +- 10 files changed, 417 insertions(+), 423 deletions(-) rename packages/react-reconciler/src/__tests__/{ReactNewContext-test.internal.js => ReactContext-test.internal.js} (99%) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index d3d479da64eab..38dc475df448a 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -80,12 +80,12 @@ import { stopBaseRenderTimerIfRunning, } from './ReactProfilerTimer'; import { - getMaskedContext, - getUnmaskedContext, - hasContextChanged as hasLegacyContextChanged, - pushContextProvider as pushLegacyContextProvider, - pushTopLevelContextObject, - invalidateContextProvider, + readUnmaskedLegacyContext, + maskLegacyContext, + emptyContextObject, + pushRootLegacyContext, + calculateLegacyChildContext, + pushLegacyContext, } from './ReactFiberContext'; import { enterHydrationState, @@ -156,14 +156,16 @@ function reconcileChildrenAtExpirationTime( } } -function updateForwardRef(current, workInProgress) { +function updateForwardRef(current, workInProgress, renderExpirationTime) { const render = workInProgress.type.render; const nextProps = workInProgress.pendingProps; const ref = workInProgress.ref; - 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) { + + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && workInProgress.memoizedProps === nextProps) { const currentRef = current !== null ? current.ref : null; if (ref === currentRef) { return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -185,12 +187,13 @@ function updateForwardRef(current, workInProgress) { 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) { + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && workInProgress.memoizedProps === nextChildren) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } reconcileChildren(current, workInProgress, nextChildren); @@ -198,14 +201,15 @@ function updateFragment(current, workInProgress) { 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 + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if ( + !hasPendingContext && + (nextChildren === null || workInProgress.memoizedProps === nextChildren) ) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } @@ -214,12 +218,16 @@ function updateMode(current, workInProgress) { return workInProgress.child; } -function updateProfiler(current, workInProgress) { +function updateProfiler(current, workInProgress, renderExpirationTime) { const nextProps = workInProgress.pendingProps; if (enableProfilerTimer) { workInProgress.effectTag |= Update; } - if (workInProgress.memoizedProps === nextProps) { + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && workInProgress.memoizedProps === nextProps) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } const nextChildren = nextProps.children; @@ -247,32 +255,40 @@ function updateFunctionalComponent( const fn = workInProgress.type; const nextProps = workInProgress.pendingProps; - if (!checkForPendingContext(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); - } - // TODO: consider bringing fn.shouldComponentUpdate() back. - // It used to be here. - } + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && workInProgress.memoizedProps === nextProps) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); } + // TODO: consider bringing fn.shouldComponentUpdate() back. + // It used to be here. + + prepareToReadContext(); - const unmaskedContext = getUnmaskedContext(workInProgress); - const context = getMaskedContext(workInProgress, unmaskedContext); + let legacyContext; + const contextTypes = fn.contextTypes; + if (typeof contextTypes === 'object' && contextTypes !== null) { + const unmaskedContext = readUnmaskedLegacyContext(); + legacyContext = maskLegacyContext( + unmaskedContext, + unmaskedContext, + contextTypes, + ); + } else { + legacyContext = emptyContextObject; + } let nextChildren; - prepareToReadContext(); if (__DEV__) { ReactCurrentOwner.current = workInProgress; ReactDebugCurrentFiber.setCurrentPhase('render'); - nextChildren = fn(nextProps, context); + nextChildren = fn(nextProps, legacyContext); ReactDebugCurrentFiber.setCurrentPhase(null); } else { - nextChildren = fn(nextProps, context); + nextChildren = fn(nextProps, legacyContext); } workInProgress.firstContextReader = finishReadingContext(); @@ -288,11 +304,33 @@ function updateClassComponent( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ) { - // Push context providers early to prevent context stack mismatches. - // 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); + // It's possible for a component to both provide and read from context. We + // should read the current context before pushing additional context onto + // the stack. prepareToReadContext(); + let maskedLegacyContext; + const contextTypes = workInProgress.type.contextTypes; + if (typeof contextTypes === 'object' && contextTypes !== null) { + const unmaskedLegacyContext = readUnmaskedLegacyContext(); + const instance = workInProgress.stateNode; + if ( + instance !== null && + instance.__reactInternalUnmaskedLegacyContext === unmaskedLegacyContext + ) { + // Avoid recreating masked context unless unmasked context has changed. + // Failing to do this will result in unnecessary calls to componentWillReceiveProps. + // This may trigger infinite loops if componentWillReceiveProps calls setState. + maskedLegacyContext = instance.__reactInternalMaskedLegacyContext; + } else { + maskedLegacyContext = maskLegacyContext( + workInProgress, + unmaskedLegacyContext, + contextTypes, + ); + } + } else { + maskedLegacyContext = emptyContextObject; + } let shouldUpdate; if (current === null) { @@ -301,15 +339,21 @@ function updateClassComponent( constructClassInstance( workInProgress, workInProgress.pendingProps, + maskedLegacyContext, + renderExpirationTime, + ); + mountClassInstance( + workInProgress, + maskedLegacyContext, renderExpirationTime, ); - mountClassInstance(workInProgress, renderExpirationTime); shouldUpdate = true; } else { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance( workInProgress, + maskedLegacyContext, renderExpirationTime, ); } @@ -317,14 +361,22 @@ function updateClassComponent( shouldUpdate = updateClassInstance( current, workInProgress, + maskedLegacyContext, renderExpirationTime, ); } + + // We can assume we have an instance at this point + const instance = workInProgress.stateNode; + if (typeof contextTypes === 'object' && contextTypes !== null) { + instance.__reactInternalUnmaskedLegacyContext = readUnmaskedLegacyContext(); + instance.__reactInternalMaskedLegacyContext = maskedLegacyContext; + } + return finishClassComponent( current, workInProgress, shouldUpdate, - hasContext, renderExpirationTime, ); } @@ -333,26 +385,35 @@ function finishClassComponent( current: Fiber | null, workInProgress: Fiber, shouldUpdate: boolean, - hasContext: boolean, renderExpirationTime: ExpirationTime, ) { + const ctor = workInProgress.type; + const childContextTypes = ctor.childContextTypes; + // Refs should update even if shouldComponentUpdate returns false markRef(current, workInProgress); const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect; + const instance = workInProgress.stateNode; if (!shouldUpdate && !didCaptureError) { - // Context providers should defer to sCU for rendering - if (hasContext) { - invalidateContextProvider(workInProgress, false); + // Call finishReadingContext to clear the current context list, but don't + // use the result. Because we're about to bail out without rendering, we + // should re-use the previous list. + finishReadingContext(); + if (typeof childContextTypes === 'object' && childContextTypes !== null) { + const legacyChildContext = + instance.__reactInternalUnmaskedLegacyChildContext; + pushLegacyContext( + workInProgress, + childContextTypes, + legacyChildContext, + false, + ); } - return bailoutOnAlreadyFinishedWork(current, workInProgress); } - const ctor = workInProgress.type; - const instance = workInProgress.stateNode; - // Rerender ReactCurrentOwner.current = workInProgress; let nextChildren; @@ -388,8 +449,6 @@ function finishClassComponent( } } - workInProgress.firstContextReader = finishReadingContext(); - // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; if (didCaptureError) { @@ -417,9 +476,21 @@ function finishClassComponent( memoizeState(workInProgress, instance.state); memoizeProps(workInProgress, instance.props); - // The context might have changed so we need to recalculate it. - if (hasContext) { - invalidateContextProvider(workInProgress, true); + workInProgress.firstContextReader = finishReadingContext(); + if (typeof childContextTypes === 'object' && childContextTypes !== null) { + const unmaskedLegacyContext = readUnmaskedLegacyContext(); + const legacyChildContext = calculateLegacyChildContext( + workInProgress, + childContextTypes, + unmaskedLegacyContext, + ); + instance.__reactInternalUnmaskedLegacyChildContext = legacyChildContext; + pushLegacyContext( + workInProgress, + childContextTypes, + legacyChildContext, + true, + ); } return workInProgress.child; @@ -427,15 +498,15 @@ function finishClassComponent( function pushHostRootContext(workInProgress) { const root = (workInProgress.stateNode: FiberRoot); - if (root.pendingContext) { - pushTopLevelContextObject( + if (root.pendingContext !== null) { + pushRootLegacyContext( workInProgress, root.pendingContext, root.pendingContext !== root.context, ); - } else if (root.context) { + } else if (root.context !== null) { // Should always be set - pushTopLevelContextObject(workInProgress, root.context, false); + pushRootLegacyContext(workInProgress, root.context, false); } pushHostContainer(workInProgress, root.containerInfo); } @@ -516,10 +587,11 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) { 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 hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && memoizedProps === nextProps) { const isHidden = workInProgress.mode & AsyncMode && shouldDeprioritizeSubtree(type, nextProps); @@ -592,11 +664,26 @@ function mountIndeterminateComponent( ); const fn = workInProgress.type; const props = workInProgress.pendingProps; - const unmaskedContext = getUnmaskedContext(workInProgress); - const context = getMaskedContext(workInProgress, unmaskedContext); prepareToReadContext(); + // It's possible for a component to both provide and read from context. We + // should read the current context before pushing additional context onto + // the stack. + prepareToReadContext(); + let maskedLegacyContext; + const contextTypes = workInProgress.type.contextTypes; + if (typeof contextTypes === 'object' && contextTypes !== null) { + const unmaskedLegacyContext = readUnmaskedLegacyContext(); + maskedLegacyContext = maskLegacyContext( + unmaskedLegacyContext, + unmaskedLegacyContext, + contextTypes, + ); + } else { + maskedLegacyContext = emptyContextObject; + } + let value; if (__DEV__) { @@ -620,9 +707,9 @@ function mountIndeterminateComponent( } ReactCurrentOwner.current = workInProgress; - value = fn(props, context); + value = fn(props, maskedLegacyContext); } else { - value = fn(props, context); + value = fn(props, maskedLegacyContext); } // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; @@ -655,14 +742,16 @@ function mountIndeterminateComponent( // Push context providers early to prevent context stack mismatches. // 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); adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, renderExpirationTime); + mountClassInstance( + workInProgress, + maskedLegacyContext, + renderExpirationTime, + ); return finishClassComponent( current, workInProgress, true, - hasContext, renderExpirationTime, ); } else { @@ -736,10 +825,15 @@ function updateTimeoutComponent(current, workInProgress, renderExpirationTime) { (workInProgress.effectTag & DidCapture) === NoEffect; const 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) { + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if ( + !hasPendingContext && + nextProps === prevProps && + nextDidTimeout === prevDidTimeout + ) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } @@ -757,10 +851,11 @@ function updateTimeoutComponent(current, workInProgress, renderExpirationTime) { 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) { + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && workInProgress.memoizedProps === nextChildren) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } @@ -790,13 +885,12 @@ 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) { + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && oldProps === newProps) { workInProgress.stateNode = 0; pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -826,7 +920,7 @@ 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 (!hasPendingContext && oldProps.children === newProps.children) { workInProgress.stateNode = 0; pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -843,7 +937,7 @@ 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 (!hasPendingContext && oldProps.children === newProps.children) { workInProgress.stateNode = 0; pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -866,7 +960,7 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { if (changedBits === 0) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children && canBailOnProps) { + if (!hasPendingContext && oldProps.children === newProps.children) { workInProgress.stateNode = 0; pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -896,13 +990,12 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - if (!checkForPendingContext(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) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && oldProps === newProps) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); } workInProgress.memoizedProps = newProps; @@ -1001,7 +1094,18 @@ function bailoutOnLowPriority(current, workInProgress) { pushHostRootContext(workInProgress); break; case ClassComponent: - pushLegacyContextProvider(workInProgress); + const childContextTypes = workInProgress.type.childContextTypes; + if (typeof childContextTypes === 'object' && childContextTypes !== null) { + const instance = workInProgress.stateNode; + const legacyChildContext = + instance.__reactInternalUnmaskedLegacyChildContext; + pushLegacyContext( + workInProgress, + childContextTypes, + legacyChildContext, + false, + ); + } break; case HostPortal: pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); @@ -1082,13 +1186,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 60365b56d8fd1..9e7150cb9996b 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -37,14 +37,7 @@ import { ForceUpdate, } from './ReactUpdateQueue'; import {NoWork} from './ReactFiberExpirationTime'; -import { - cacheContext, - getMaskedContext, - getUnmaskedContext, - isContextConsumer, - hasContextChanged, - emptyContextObject, -} from './ReactFiberContext'; +// TODO: Maybe we can remove this? import { requestCurrentTime, computeExpirationForFiber, @@ -468,14 +461,10 @@ function adoptClassInstance(workInProgress: Fiber, instance: any): void { function constructClassInstance( workInProgress: Fiber, props: any, + legacyContext: Object, renderExpirationTime: ExpirationTime, ): any { const ctor = workInProgress.type; - const unmaskedContext = getUnmaskedContext(workInProgress); - const needsContext = isContextConsumer(workInProgress); - const context = needsContext - ? getMaskedContext(workInProgress, unmaskedContext) - : emptyContextObject; // Instantiate twice to help detect side-effects. if (__DEV__) { @@ -484,11 +473,11 @@ function constructClassInstance( (debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictMode) ) { - new ctor(props, context); // eslint-disable-line no-new + new ctor(props, legacyContext); // eslint-disable-line no-new } } - const instance = new ctor(props, context); + const instance = new ctor(props, legacyContext); const state = (workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state @@ -577,12 +566,6 @@ function constructClassInstance( } } - // Cache unmasked context so we can avoid recreating masked context unless necessary. - // ReactFiberContext usually updates this cache but can't for newly-created instances. - if (needsContext) { - cacheContext(workInProgress, unmaskedContext, context); - } - return instance; } @@ -650,6 +633,7 @@ function callComponentWillReceiveProps( // Invokes the mount life-cycles on a previously never rendered instance. function mountClassInstance( workInProgress: Fiber, + legacyContext: Object, renderExpirationTime: ExpirationTime, ): void { const ctor = workInProgress.type; @@ -660,12 +644,11 @@ function mountClassInstance( const instance = workInProgress.stateNode; const props = workInProgress.pendingProps; - const unmaskedContext = getUnmaskedContext(workInProgress); instance.props = props; instance.state = workInProgress.memoizedState; instance.refs = emptyRefsObject; - instance.context = getMaskedContext(workInProgress, unmaskedContext); + instance.context = legacyContext; if (__DEV__) { if (workInProgress.mode & StrictMode) { @@ -737,6 +720,7 @@ function mountClassInstance( function resumeMountClassInstance( workInProgress: Fiber, + nextLegacyContext: Object, renderExpirationTime: ExpirationTime, ): boolean { const ctor = workInProgress.type; @@ -747,13 +731,8 @@ function resumeMountClassInstance( instance.props = oldProps; const oldContext = instance.context; - const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress); - const nextLegacyContext = getMaskedContext( - workInProgress, - nextLegacyUnmaskedContext, - ); - const hasPendingNewContext = checkForPendingContext( + const hasPendingContext = checkForPendingContext( workInProgress, renderExpirationTime, ); @@ -802,8 +781,7 @@ function resumeMountClassInstance( if ( oldProps === newProps && oldState === newState && - !hasContextChanged() && - !hasPendingNewContext && + !hasPendingContext && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update @@ -825,7 +803,6 @@ function resumeMountClassInstance( const shouldUpdate = checkHasForceUpdateAfterProcessing() || - hasPendingNewContext || checkShouldComponentUpdate( workInProgress, oldProps, @@ -881,6 +858,7 @@ function resumeMountClassInstance( function updateClassInstance( current: Fiber, workInProgress: Fiber, + nextLegacyContext: Object, renderExpirationTime: ExpirationTime, ): boolean { const ctor = workInProgress.type; @@ -891,13 +869,8 @@ function updateClassInstance( instance.props = oldProps; const oldContext = instance.context; - const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress); - const nextLegacyContext = getMaskedContext( - workInProgress, - nextLegacyUnmaskedContext, - ); - const hasPendingNewContext = checkForPendingContext( + const hasPendingContext = checkForPendingContext( workInProgress, renderExpirationTime, ); @@ -947,8 +920,7 @@ function updateClassInstance( if ( oldProps === newProps && oldState === newState && - !hasContextChanged() && - !hasPendingNewContext && + !hasPendingContext && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update @@ -983,7 +955,6 @@ function updateClassInstance( const shouldUpdate = checkHasForceUpdateAfterProcessing() || - hasPendingNewContext || checkShouldComponentUpdate( workInProgress, oldProps, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index e8db8f19e8432..8a0eef4846015 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -61,10 +61,7 @@ import { popHostContainer, } from './ReactFiberHostContext'; import {recordElapsedActualRenderTime} from './ReactProfilerTimer'; -import { - popContextProvider as popLegacyContextProvider, - popTopLevelContextObject as popTopLevelLegacyContextObject, -} from './ReactFiberContext'; +import {popLegacyContext, popRootLegacyContext} from './ReactFiberContext'; import {popProvider} from './ReactFiberNewContext'; import { prepareToHydrateHostInstance, @@ -325,12 +322,12 @@ function completeWork( return null; case ClassComponent: { // We are leaving this subtree, so pop context if any. - popLegacyContextProvider(workInProgress); + popLegacyContext(workInProgress); return null; } case HostRoot: { popHostContainer(workInProgress); - popTopLevelLegacyContextObject(workInProgress); + popRootLegacyContext(workInProgress); const fiberRoot = (workInProgress.stateNode: FiberRoot); if (fiberRoot.pendingContext) { fiberRoot.context = fiberRoot.pendingContext; diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 1a1a30f35e014..166f563a05464 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -8,8 +8,7 @@ */ import type {Fiber} from './ReactFiber'; -import type {StackCursor} from './ReactFiberStack'; - +import type {ReactContext} from 'shared/ReactTypes'; import {isFiberMounted} from 'react-reconciler/reflection'; import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; @@ -19,146 +18,60 @@ import checkPropTypes from 'prop-types/checkPropTypes'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; -import {createCursor, push, pop} from './ReactFiberStack'; +import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; +import {readContext, pushContext, popContext} from './ReactFiberNewContext'; +import maxSigned31BitInt from './maxSigned31BitInt'; +import {DidThrow, NoEffect} from 'shared/ReactTypeOfSideEffect'; let warnedAboutMissingGetChildContext; if (__DEV__) { warnedAboutMissingGetChildContext = {}; } - export const emptyContextObject = {}; if (__DEV__) { Object.freeze(emptyContextObject); } -// A cursor to the current merged context object on the stack. -let contextStackCursor: StackCursor = createCursor(emptyContextObject); -// A cursor to a boolean indicating whether the context has changed. -let didPerformWorkStackCursor: StackCursor = createCursor(false); -// Keep track of the previous context object that was on the stack. -// We use this to get access to the parent context after we have already -// pushed the next context provider, and now need to merge their contexts. -let previousContext: Object = emptyContextObject; +export const LegacyContext: ReactContext = { + $$typeof: REACT_CONTEXT_TYPE, + _calculateChangedBits: null, + _defaultValue: emptyContextObject, + _currentValue: emptyContextObject, + _currentValue2: emptyContextObject, + _changedBits: 0, + _changedBits2: 0, + // These are circular + Provider: (null: any), + Consumer: (null: any), + unstable_read: (null: any), +}; -function getUnmaskedContext(workInProgress: Fiber): Object { - const hasOwnContext = isContextProvider(workInProgress); - if (hasOwnContext) { - // If the fiber is a context provider itself, when we read its context - // we have already pushed its own child context on the stack. A context - // provider should not "see" its own child context. Therefore we read the - // previous (parent) context instead for a context provider. - return previousContext; - } - return contextStackCursor.current; -} +LegacyContext.Provider = { + $$typeof: REACT_PROVIDER_TYPE, + _context: LegacyContext, +}; +LegacyContext.Consumer = LegacyContext; +LegacyContext.unstable_read = readContext.bind(null, LegacyContext); -function cacheContext( - workInProgress: Fiber, - unmaskedContext: Object, - maskedContext: Object, -): void { - const instance = workInProgress.stateNode; - instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; - instance.__reactInternalMemoizedMaskedChildContext = maskedContext; +if (__DEV__) { + LegacyContext._currentRenderer = null; + LegacyContext._currentRenderer2 = null; } -function getMaskedContext( +export function calculateLegacyChildContext( workInProgress: Fiber, - unmaskedContext: Object, + childContextTypes: Object, + unmaskedParentContext: Object, ): Object { - const type = workInProgress.type; - const contextTypes = type.contextTypes; - if (!contextTypes) { - return emptyContextObject; - } - - // Avoid recreating masked context unless unmasked context has changed. - // Failing to do this will result in unnecessary calls to componentWillReceiveProps. - // This may trigger infinite loops if componentWillReceiveProps calls setState. const instance = workInProgress.stateNode; - if ( - instance && - instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext - ) { - return instance.__reactInternalMemoizedMaskedChildContext; - } - - const context = {}; - for (let key in contextTypes) { - context[key] = unmaskedContext[key]; - } - - if (__DEV__) { - const name = getComponentName(workInProgress) || 'Unknown'; - checkPropTypes( - contextTypes, - context, - 'context', - name, - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, - ); - } - - // Cache unmasked context so we can avoid recreating masked context unless necessary. - // Context is created before the class component is instantiated so check for instance. - if (instance) { - cacheContext(workInProgress, unmaskedContext, context); - } - - return context; -} - -function hasContextChanged(): boolean { - return didPerformWorkStackCursor.current; -} - -function isContextConsumer(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.contextTypes != null; -} - -function isContextProvider(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.childContextTypes != null; -} - -function popContextProvider(fiber: Fiber): void { - if (!isContextProvider(fiber)) { - return; - } - - pop(didPerformWorkStackCursor, fiber); - pop(contextStackCursor, fiber); -} - -function popTopLevelContextObject(fiber: Fiber): void { - pop(didPerformWorkStackCursor, fiber); - pop(contextStackCursor, fiber); -} - -function pushTopLevelContextObject( - fiber: Fiber, - context: Object, - didChange: boolean, -): void { - invariant( - contextStackCursor.current === emptyContextObject, - 'Unexpected context found on stack. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - - push(contextStackCursor, context, fiber); - push(didPerformWorkStackCursor, didChange, fiber); -} - -function processChildContext(fiber: Fiber, parentContext: Object): Object { - const instance = fiber.stateNode; - const childContextTypes = fiber.type.childContextTypes; + let childContext; // TODO (bvaughn) Replace this behavior with an invariant() in the future. // It has only been added in Fiber to match the (unintentional) behavior in Stack. if (typeof instance.getChildContext !== 'function') { if (__DEV__) { - const componentName = getComponentName(fiber) || 'Unknown'; + const componentName = getComponentName(workInProgress) || 'Unknown'; if (!warnedAboutMissingGetChildContext[componentName]) { warnedAboutMissingGetChildContext[componentName] = true; @@ -172,104 +85,109 @@ function processChildContext(fiber: Fiber, parentContext: Object): Object { ); } } - return parentContext; + childContext = unmaskedParentContext; + } else { + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); + } + startPhaseTimer(workInProgress, 'getChildContext'); + childContext = instance.getChildContext(); + stopPhaseTimer(); + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase(null); + } + for (let contextKey in childContext) { + invariant( + contextKey in childContextTypes, + '%s.getChildContext(): key "%s" is not defined in childContextTypes.', + getComponentName(workInProgress) || 'Unknown', + contextKey, + ); + } + if (__DEV__) { + const name = getComponentName(workInProgress) || 'Unknown'; + checkPropTypes( + childContextTypes, + childContext, + 'child context', + name, + // In practice, there is one case in which we won't get a stack. It's when + // somebody calls unstable_renderSubtreeIntoContainer() and we process + // context from the parent component instance. The stack will be missing + // because it's outside of the reconciliation, and so the pointer has not + // been set. This is rare and doesn't matter. We'll also remove that API. + ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + ); + } + childContext = Object.assign({}, unmaskedParentContext, childContext); } - let childContext; - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); - } - startPhaseTimer(fiber, 'getChildContext'); - childContext = instance.getChildContext(); - stopPhaseTimer(); - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase(null); - } - for (let contextKey in childContext) { - invariant( - contextKey in childContextTypes, - '%s.getChildContext(): key "%s" is not defined in childContextTypes.', - getComponentName(fiber) || 'Unknown', - contextKey, - ); - } - if (__DEV__) { - const name = getComponentName(fiber) || 'Unknown'; - checkPropTypes( - childContextTypes, - childContext, - 'child context', - name, - // In practice, there is one case in which we won't get a stack. It's when - // somebody calls unstable_renderSubtreeIntoContainer() and we process - // context from the parent component instance. The stack will be missing - // because it's outside of the reconciliation, and so the pointer has not - // been set. This is rare and doesn't matter. We'll also remove that API. - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, - ); - } + return childContext; +} - return {...parentContext, ...childContext}; +export function pushLegacyContext( + workInProgress: Fiber, + childContextTypes: Object, + childContext: Object, + didChange: boolean, +): void { + const changedBits = didChange ? maxSigned31BitInt : 0; + pushContext(workInProgress, LegacyContext, childContext, changedBits); } -function pushContextProvider(workInProgress: Fiber): boolean { - if (!isContextProvider(workInProgress)) { - return false; +export function popLegacyContext(workInProgress: Fiber): void { + // Legacy context providers do not push their child context until the end of + // the render phase. If the render phase did not complete, the child context + // was never pushed. + if ((workInProgress.effectTag & DidThrow) === NoEffect) { + const childContextTypes = workInProgress.type.childContextTypes; + if (typeof childContextTypes === 'object' && childContextTypes != null) { + popContext(workInProgress, LegacyContext); + } } - - const instance = workInProgress.stateNode; - // We push the context as early as possible to ensure stack integrity. - // If the instance does not exist yet, we will push null at first, - // and replace it on the stack later when invalidating the context. - const memoizedMergedChildContext = - (instance && instance.__reactInternalMemoizedMergedChildContext) || - emptyContextObject; - - // Remember the parent context so we can merge with it later. - // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates. - previousContext = contextStackCursor.current; - push(contextStackCursor, memoizedMergedChildContext, workInProgress); - push( - didPerformWorkStackCursor, - didPerformWorkStackCursor.current, - workInProgress, - ); - - return true; } -function invalidateContextProvider( +export function pushRootLegacyContext( workInProgress: Fiber, + rootContext: Object, didChange: boolean, ): void { - const instance = workInProgress.stateNode; - invariant( - instance, - 'Expected to have an instance by this point. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); + const changedBits = didChange ? maxSigned31BitInt : 0; + pushContext(workInProgress, LegacyContext, rootContext, changedBits); +} - if (didChange) { - // Merge parent and own context. - // Skip this if we're not updating due to sCU. - // This avoids unnecessarily recomputing memoized values. - const mergedContext = processChildContext(workInProgress, previousContext); - instance.__reactInternalMemoizedMergedChildContext = mergedContext; +export function popRootLegacyContext(workInProgress: Fiber): void { + popContext(workInProgress, LegacyContext); +} - // Replace the old (or empty) context with the new one. - // It is important to unwind the context in the reverse order. - pop(didPerformWorkStackCursor, workInProgress); - pop(contextStackCursor, workInProgress); - // Now push the new context and mark that it has changed. - push(contextStackCursor, mergedContext, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); - } else { - pop(didPerformWorkStackCursor, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); +export function readUnmaskedLegacyContext(): Object { + return readContext(LegacyContext); +} + +export function maskLegacyContext( + workInProgress: Fiber, + unmaskedContext: Object, + contextTypes: Object, +): Object { + const maskedContext = {}; + for (let key in contextTypes) { + maskedContext[key] = unmaskedContext[key]; } + + if (__DEV__) { + const name = getComponentName(workInProgress) || 'Unknown'; + checkPropTypes( + contextTypes, + maskedContext, + 'context', + name, + ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + ); + } + return maskedContext; } -function findCurrentUnmaskedContext(fiber: Fiber): Object { +export function findCurrentUnmaskedContext(fiber: Fiber): Object { // Currently this is only used with renderSubtreeIntoContainer; not sure if it // makes sense elsewhere invariant( @@ -280,8 +198,8 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object { let node: Fiber = fiber; while (node.tag !== HostRoot) { - if (isContextProvider(node)) { - return node.stateNode.__reactInternalMemoizedMergedChildContext; + if (node.tag === ClassComponent && node.type.childContextTypes != null) { + return node.stateNode.__reactInternalUnmaskedLegacyChildContext; } const parent = node.return; invariant( @@ -293,19 +211,3 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object { } return node.stateNode.context; } - -export { - getUnmaskedContext, - cacheContext, - getMaskedContext, - hasContextChanged, - isContextConsumer, - isContextProvider, - popContextProvider, - popTopLevelContextObject, - pushTopLevelContextObject, - processChildContext, - pushContextProvider, - invalidateContextProvider, - findCurrentUnmaskedContext, -}; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 02a01c652c451..625168afca905 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -28,8 +28,8 @@ import {createCursor, push, pop} from './ReactFiberStack'; import maxSigned31BitInt from './maxSigned31BitInt'; import {NoWork} from './ReactFiberExpirationTime'; import {ContextProvider} from 'shared/ReactTypeOfWork'; +import {LegacyContext} from './ReactFiberContext'; -const providerCursor: StackCursor = createCursor(null); const valueCursor: StackCursor = createCursor(null); const changedBitsCursor: StackCursor = createCursor(0); @@ -41,14 +41,23 @@ if (__DEV__) { export function pushProvider(providerFiber: Fiber): void { const context: ReactContext = providerFiber.type._context; + const value = providerFiber.pendingProps.value; + const changedBits = providerFiber.stateNode; + pushContext(providerFiber, context, value, changedBits); +} +export function pushContext( + workInProgress: Fiber, + context: ReactContext, + value: T, + changedBits: number, +): void { if (isPrimaryRenderer) { - push(changedBitsCursor, context._changedBits, providerFiber); - push(valueCursor, context._currentValue, providerFiber); - push(providerCursor, providerFiber, providerFiber); + push(changedBitsCursor, context._changedBits, workInProgress); + push(valueCursor, context._currentValue, workInProgress); - context._currentValue = providerFiber.pendingProps.value; - context._changedBits = providerFiber.stateNode; + context._currentValue = value; + context._changedBits = changedBits; if (__DEV__) { warning( context._currentRenderer === undefined || @@ -60,12 +69,11 @@ export function pushProvider(providerFiber: Fiber): void { context._currentRenderer = rendererSigil; } } else { - push(changedBitsCursor, context._changedBits2, providerFiber); - push(valueCursor, context._currentValue2, providerFiber); - push(providerCursor, providerFiber, providerFiber); + push(changedBitsCursor, context._changedBits2, workInProgress); + push(valueCursor, context._currentValue2, workInProgress); - context._currentValue2 = providerFiber.pendingProps.value; - context._changedBits2 = providerFiber.stateNode; + context._currentValue2 = value; + context._changedBits2 = changedBits; if (__DEV__) { warning( context._currentRenderer2 === undefined || @@ -80,14 +88,20 @@ export function pushProvider(providerFiber: Fiber): void { } export function popProvider(providerFiber: Fiber): void { + const context: ReactContext = providerFiber.type._context; + popContext(providerFiber, context); +} + +export function popContext( + workInProgress: Fiber, + context: ReactContext, +): void { const changedBits = changedBitsCursor.current; const currentValue = valueCursor.current; - pop(providerCursor, providerFiber); - pop(valueCursor, providerFiber); - pop(changedBitsCursor, providerFiber); + pop(valueCursor, workInProgress); + pop(changedBitsCursor, workInProgress); - const context: ReactContext = providerFiber.type._context; if (isPrimaryRenderer) { context._currentValue = currentValue; context._changedBits = changedBits; @@ -221,7 +235,8 @@ export function checkForPendingContext( } reader = reader.next; } - return hasPendingContext; + + return hasPendingContext || getContextChangedBits(LegacyContext) !== 0; } export function prepareToReadContext(): void { @@ -267,6 +282,10 @@ export function readContext( return isPrimaryRenderer ? context._currentValue : context._currentValue2; } +export function getContextChangedBits(context: ReactContext): number { + return isPrimaryRenderer ? context._changedBits : context._changedBits2; +} + export function finishReadingContext(): ContextReader | null { const list = nextFirstReader; nextFirstReader = nextLastReader = null; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 245b1654e295e..5aed379659d4e 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -23,7 +23,7 @@ import { findCurrentHostFiberWithNoPortals, } from 'react-reconciler/reflection'; import * as ReactInstanceMap from 'shared/ReactInstanceMap'; -import {HostComponent} from 'shared/ReactTypeOfWork'; +import {HostComponent, ClassComponent} from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warning from 'shared/warning'; @@ -31,9 +31,8 @@ import warning from 'shared/warning'; import {getPublicInstance} from './ReactFiberHostConfig'; import { findCurrentUnmaskedContext, - isContextProvider, - processChildContext, emptyContextObject, + maskLegacyContext, } from './ReactFiberContext'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; @@ -90,10 +89,14 @@ function getContextForSubtree( } const fiber = ReactInstanceMap.get(parentComponent); - const parentContext = findCurrentUnmaskedContext(fiber); - return isContextProvider(fiber) - ? processChildContext(fiber, parentContext) - : parentContext; + const unmaskedContext = findCurrentUnmaskedContext(fiber); + if (fiber.tag === ClassComponent) { + const childContextTypes = fiber.type.childContextTypes; + if (typeof childContextTypes === 'object' && childContextTypes !== null) { + return maskLegacyContext(fiber, unmaskedContext, childContextTypes); + } + } + return unmaskedContext; } function scheduleRootUpdate( diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 5f82daa6a1ec7..50828d2bd216d 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -95,10 +95,7 @@ import { import {AsyncMode, ProfileMode} from './ReactTypeOfMode'; import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import { - popTopLevelContextObject as popTopLevelLegacyContextObject, - popContextProvider as popLegacyContextProvider, -} from './ReactFiberContext'; +import {popRootLegacyContext} from './ReactFiberContext'; import {popProvider} from './ReactFiberNewContext'; import {popHostContext, popHostContainer} from './ReactFiberHostContext'; import { @@ -280,13 +277,15 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { switch (failedUnitOfWork.tag) { case HostRoot: popHostContainer(failedUnitOfWork); - popTopLevelLegacyContextObject(failedUnitOfWork); + popRootLegacyContext(failedUnitOfWork); break; case HostComponent: popHostContext(failedUnitOfWork); break; case ClassComponent: - popLegacyContextProvider(failedUnitOfWork); + // Legacy context providers do not push their child context until the + // end of the render phase. Since the render phase did not complete, the + // child context was never pushed. Do not pop. break; case HostPortal: popHostContainer(failedUnitOfWork); diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 4d0c2331c0bbf..2a0f69becabc0 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -27,6 +27,7 @@ import { Incomplete, NoEffect, ShouldCapture, + DidThrow, } from 'shared/ReactTypeOfSideEffect'; import { enableGetDerivedStateFromCatch, @@ -45,10 +46,7 @@ import { import {logError} from './ReactFiberCommitWork'; import {Never, Sync, expirationTimeToMs} from './ReactFiberExpirationTime'; import {popHostContainer, popHostContext} from './ReactFiberHostContext'; -import { - popContextProvider as popLegacyContextProvider, - popTopLevelContextObject as popTopLevelLegacyContextObject, -} from './ReactFiberContext'; +import {popLegacyContext, popRootLegacyContext} from './ReactFiberContext'; import {popProvider} from './ReactFiberNewContext'; import { resumeActualRenderTimerIfPaused, @@ -147,7 +145,7 @@ function throwException( renderExpirationTime: ExpirationTime, ) { // The source fiber did not complete. - sourceFiber.effectTag |= Incomplete; + sourceFiber.effectTag |= Incomplete | DidThrow; // Its effect list is no longer valid. sourceFiber.firstEffect = sourceFiber.lastEffect = null; @@ -325,7 +323,7 @@ function unwindWork( switch (workInProgress.tag) { case ClassComponent: { - popLegacyContextProvider(workInProgress); + popLegacyContext(workInProgress); const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; @@ -335,7 +333,7 @@ function unwindWork( } case HostRoot: { popHostContainer(workInProgress); - popTopLevelLegacyContextObject(workInProgress); + popRootLegacyContext(workInProgress); const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; @@ -377,12 +375,12 @@ function unwindInterruptedWork(interruptedWork: Fiber) { switch (interruptedWork.tag) { case ClassComponent: { - popLegacyContextProvider(interruptedWork); + popLegacyContext(interruptedWork); break; } case HostRoot: { popHostContainer(interruptedWork); - popTopLevelLegacyContextObject(interruptedWork); + popRootLegacyContext(interruptedWork); break; } case HostComponent: { diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactContext-test.internal.js similarity index 99% rename from packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js rename to packages/react-reconciler/src/__tests__/ReactContext-test.internal.js index e090239540bd6..b73401b9b929e 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactContext-test.internal.js @@ -15,7 +15,7 @@ let React = require('react'); let ReactNoop; let gen; -describe('ReactNewContext', () => { +describe('ReactContext', () => { beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); diff --git a/packages/shared/ReactTypeOfSideEffect.js b/packages/shared/ReactTypeOfSideEffect.js index 27d6aa6090e45..b9329fc73ea35 100644 --- a/packages/shared/ReactTypeOfSideEffect.js +++ b/packages/shared/ReactTypeOfSideEffect.js @@ -10,22 +10,23 @@ export type TypeOfSideEffect = number; // Don't change these two values. They're used by React Dev Tools. -export const NoEffect = /* */ 0b00000000000; -export const PerformedWork = /* */ 0b00000000001; +export const NoEffect = /* */ 0b000000000000; +export const PerformedWork = /* */ 0b000000000001; // You can change the rest (and add more). -export const Placement = /* */ 0b00000000010; -export const Update = /* */ 0b00000000100; -export const PlacementAndUpdate = /* */ 0b00000000110; -export const Deletion = /* */ 0b00000001000; -export const ContentReset = /* */ 0b00000010000; -export const Callback = /* */ 0b00000100000; -export const DidCapture = /* */ 0b00001000000; -export const Ref = /* */ 0b00010000000; -export const Snapshot = /* */ 0b00100000000; +export const Placement = /* */ 0b000000000010; +export const Update = /* */ 0b000000000100; +export const PlacementAndUpdate = /* */ 0b000000000110; +export const Deletion = /* */ 0b000000001000; +export const ContentReset = /* */ 0b000000010000; +export const Callback = /* */ 0b000000100000; +export const DidCapture = /* */ 0b000001000000; +export const Ref = /* */ 0b000010000000; +export const Snapshot = /* */ 0b000100000000; // Union of all host effects -export const HostEffectMask = /* */ 0b00111111111; +export const HostEffectMask = /* */ 0b000111111111; -export const Incomplete = /* */ 0b01000000000; -export const ShouldCapture = /* */ 0b10000000000; +export const DidThrow = /* */ 0b001000000000; +export const Incomplete = /* */ 0b010000000000; +export const ShouldCapture = /* */ 0b100000000000; From eb840451f73007b748314080f7cfe90205a6c8ca Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 2 Jul 2018 23:52:38 -0700 Subject: [PATCH 4/4] Rename context files Doing this in a separate commit so the diff is less confusing --- packages/react-reconciler/src/ReactFiber.js | 2 +- .../src/ReactFiberBeginWork.js | 4 +- .../src/ReactFiberClassComponent.js | 2 +- .../src/ReactFiberCompleteWork.js | 7 +- .../react-reconciler/src/ReactFiberContext.js | 404 +++++++++++------- .../src/ReactFiberDispatcher.js | 2 +- .../src/ReactFiberLegacyContext.js | 213 +++++++++ .../src/ReactFiberNewContext.js | 293 ------------- .../src/ReactFiberReconciler.js | 2 +- .../src/ReactFiberScheduler.js | 4 +- .../src/ReactFiberUnwindWork.js | 7 +- 11 files changed, 473 insertions(+), 467 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberLegacyContext.js delete mode 100644 packages/react-reconciler/src/ReactFiberNewContext.js diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index f65fd107b1cd7..978350cd55c31 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -14,7 +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 {ContextReader} from './ReactFiberNewContext'; +import type {ContextReader} from './ReactFiberContext'; import invariant from 'shared/invariant'; import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 38dc475df448a..1bdd82332f8ad 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -74,7 +74,7 @@ import { readContext, prepareToReadContext, finishReadingContext, -} from './ReactFiberNewContext'; +} from './ReactFiberContext'; import { markActualRenderTimeStarted, stopBaseRenderTimerIfRunning, @@ -86,7 +86,7 @@ import { pushRootLegacyContext, calculateLegacyChildContext, pushLegacyContext, -} from './ReactFiberContext'; +} from './ReactFiberLegacyContext'; import { enterHydrationState, resetHydrationState, diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 9e7150cb9996b..99dc1f8424ab3 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -43,7 +43,7 @@ import { computeExpirationForFiber, scheduleWork, } from './ReactFiberScheduler'; -import {checkForPendingContext} from './ReactFiberNewContext'; +import {checkForPendingContext} from './ReactFiberContext'; const fakeInternalInstance = {}; const isArray = Array.isArray; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 8a0eef4846015..d6d1c4142a4fd 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -61,8 +61,11 @@ import { popHostContainer, } from './ReactFiberHostContext'; import {recordElapsedActualRenderTime} from './ReactProfilerTimer'; -import {popLegacyContext, popRootLegacyContext} from './ReactFiberContext'; -import {popProvider} from './ReactFiberNewContext'; +import { + popLegacyContext, + popRootLegacyContext, +} from './ReactFiberLegacyContext'; +import {popProvider} from './ReactFiberContext'; import { prepareToHydrateHostInstance, prepareToHydrateHostTextInstance, diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 166f563a05464..7cca8c0a17698 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -7,207 +7,287 @@ * @flow */ -import type {Fiber} from './ReactFiber'; import type {ReactContext} from 'shared/ReactTypes'; -import {isFiberMounted} from 'react-reconciler/reflection'; -import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork'; -import getComponentName from 'shared/getComponentName'; -import invariant from 'shared/invariant'; +import type {Fiber} from './ReactFiber'; +import type {StackCursor} from './ReactFiberStack'; +import type {ExpirationTime} from './ReactFiberExpirationTime'; + +export type ContextReader = { + context: ReactContext, + observedBits: number, + next: ContextReader | null, +}; + +let nextFirstReader: ContextReader | null = null; +let nextLastReader: ContextReader | null = null; + import warning from 'shared/warning'; -import checkPropTypes from 'prop-types/checkPropTypes'; -import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; -import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; -import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; -import {readContext, pushContext, popContext} from './ReactFiberNewContext'; +import {isPrimaryRenderer} from './ReactFiberHostConfig'; +import {createCursor, push, pop} from './ReactFiberStack'; import maxSigned31BitInt from './maxSigned31BitInt'; -import {DidThrow, NoEffect} from 'shared/ReactTypeOfSideEffect'; +import {NoWork} from './ReactFiberExpirationTime'; +import {ContextProvider} from 'shared/ReactTypeOfWork'; +import {LegacyContext} from './ReactFiberLegacyContext'; -let warnedAboutMissingGetChildContext; +const valueCursor: StackCursor = createCursor(null); +const changedBitsCursor: StackCursor = createCursor(0); +let rendererSigil; if (__DEV__) { - warnedAboutMissingGetChildContext = {}; + // Use this to detect multiple renderers using the same context + rendererSigil = {}; } -export const emptyContextObject = {}; -if (__DEV__) { - Object.freeze(emptyContextObject); -} - -export const LegacyContext: ReactContext = { - $$typeof: REACT_CONTEXT_TYPE, - _calculateChangedBits: null, - _defaultValue: emptyContextObject, - _currentValue: emptyContextObject, - _currentValue2: emptyContextObject, - _changedBits: 0, - _changedBits2: 0, - // These are circular - Provider: (null: any), - Consumer: (null: any), - unstable_read: (null: any), -}; -LegacyContext.Provider = { - $$typeof: REACT_PROVIDER_TYPE, - _context: LegacyContext, -}; -LegacyContext.Consumer = LegacyContext; -LegacyContext.unstable_read = readContext.bind(null, LegacyContext); - -if (__DEV__) { - LegacyContext._currentRenderer = null; - LegacyContext._currentRenderer2 = null; +export function pushProvider(providerFiber: Fiber): void { + const context: ReactContext = providerFiber.type._context; + const value = providerFiber.pendingProps.value; + const changedBits = providerFiber.stateNode; + pushContext(providerFiber, context, value, changedBits); } -export function calculateLegacyChildContext( +export function pushContext( workInProgress: Fiber, - childContextTypes: Object, - unmaskedParentContext: Object, -): Object { - const instance = workInProgress.stateNode; - - let childContext; - // TODO (bvaughn) Replace this behavior with an invariant() in the future. - // It has only been added in Fiber to match the (unintentional) behavior in Stack. - if (typeof instance.getChildContext !== 'function') { - if (__DEV__) { - const componentName = getComponentName(workInProgress) || 'Unknown'; - - if (!warnedAboutMissingGetChildContext[componentName]) { - warnedAboutMissingGetChildContext[componentName] = true; - warning( - false, - '%s.childContextTypes is specified but there is no getChildContext() method ' + - 'on the instance. You can either define getChildContext() on %s or remove ' + - 'childContextTypes from it.', - componentName, - componentName, - ); - } - } - childContext = unmaskedParentContext; - } else { - if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); - } - startPhaseTimer(workInProgress, 'getChildContext'); - childContext = instance.getChildContext(); - stopPhaseTimer(); + context: ReactContext, + value: T, + changedBits: number, +): void { + if (isPrimaryRenderer) { + push(changedBitsCursor, context._changedBits, workInProgress); + push(valueCursor, context._currentValue, workInProgress); + + context._currentValue = value; + context._changedBits = changedBits; if (__DEV__) { - ReactDebugCurrentFiber.setCurrentPhase(null); - } - for (let contextKey in childContext) { - invariant( - contextKey in childContextTypes, - '%s.getChildContext(): key "%s" is not defined in childContextTypes.', - getComponentName(workInProgress) || 'Unknown', - contextKey, + warning( + context._currentRenderer === undefined || + context._currentRenderer === null || + context._currentRenderer === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', ); + context._currentRenderer = rendererSigil; } + } else { + push(changedBitsCursor, context._changedBits2, workInProgress); + push(valueCursor, context._currentValue2, workInProgress); + + context._currentValue2 = value; + context._changedBits2 = changedBits; if (__DEV__) { - const name = getComponentName(workInProgress) || 'Unknown'; - checkPropTypes( - childContextTypes, - childContext, - 'child context', - name, - // In practice, there is one case in which we won't get a stack. It's when - // somebody calls unstable_renderSubtreeIntoContainer() and we process - // context from the parent component instance. The stack will be missing - // because it's outside of the reconciliation, and so the pointer has not - // been set. This is rare and doesn't matter. We'll also remove that API. - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + warning( + context._currentRenderer2 === undefined || + context._currentRenderer2 === null || + context._currentRenderer2 === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', ); + context._currentRenderer2 = rendererSigil; } - childContext = Object.assign({}, unmaskedParentContext, childContext); } +} - return childContext; +export function popProvider(providerFiber: Fiber): void { + const context: ReactContext = providerFiber.type._context; + popContext(providerFiber, context); } -export function pushLegacyContext( +export function popContext( workInProgress: Fiber, - childContextTypes: Object, - childContext: Object, - didChange: boolean, + context: ReactContext, ): void { - const changedBits = didChange ? maxSigned31BitInt : 0; - pushContext(workInProgress, LegacyContext, childContext, changedBits); -} + const changedBits = changedBitsCursor.current; + const currentValue = valueCursor.current; -export function popLegacyContext(workInProgress: Fiber): void { - // Legacy context providers do not push their child context until the end of - // the render phase. If the render phase did not complete, the child context - // was never pushed. - if ((workInProgress.effectTag & DidThrow) === NoEffect) { - const childContextTypes = workInProgress.type.childContextTypes; - if (typeof childContextTypes === 'object' && childContextTypes != null) { - popContext(workInProgress, LegacyContext); - } + pop(valueCursor, workInProgress); + pop(changedBitsCursor, workInProgress); + + if (isPrimaryRenderer) { + context._currentValue = currentValue; + context._changedBits = changedBits; + } else { + context._currentValue2 = currentValue; + context._changedBits2 = changedBits; } } -export function pushRootLegacyContext( +export function propagateContextChange( workInProgress: Fiber, - rootContext: Object, - didChange: boolean, + context: ReactContext, + changedBits: number, + renderExpirationTime: ExpirationTime, ): void { - const changedBits = didChange ? maxSigned31BitInt : 0; - pushContext(workInProgress, LegacyContext, rootContext, changedBits); -} + 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; -export function popRootLegacyContext(workInProgress: Fiber): void { - popContext(workInProgress, LegacyContext); -} + // Visit this fiber. + let reader = fiber.firstContextReader; + if (reader !== null) { + do { + // Check if the context matches. + if ( + reader.context === context && + (reader.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; + } + reader = reader.next; + } while (reader !== 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; + } -export function readUnmaskedLegacyContext(): Object { - return readContext(LegacyContext); + 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; + } } -export function maskLegacyContext( +export function checkForPendingContext( workInProgress: Fiber, - unmaskedContext: Object, - contextTypes: Object, -): Object { - const maskedContext = {}; - for (let key in contextTypes) { - maskedContext[key] = unmaskedContext[key]; + renderExpirationTime: ExpirationTime, +): boolean { + let reader = workInProgress.firstContextReader; + let hasPendingContext = false; + while (reader !== null) { + const context = reader.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 & reader.observedBits) !== 0) { + hasPendingContext = true; + } + } + reader = reader.next; } - if (__DEV__) { - const name = getComponentName(workInProgress) || 'Unknown'; - checkPropTypes( - contextTypes, - maskedContext, - 'context', - name, - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, - ); - } - return maskedContext; + return hasPendingContext || getContextChangedBits(LegacyContext) !== 0; +} + +export function prepareToReadContext(): void { + nextFirstReader = nextLastReader = null; } -export function findCurrentUnmaskedContext(fiber: Fiber): Object { - // Currently this is only used with renderSubtreeIntoContainer; not sure if it - // makes sense elsewhere - invariant( - isFiberMounted(fiber) && fiber.tag === ClassComponent, - 'Expected subtree parent to be a mounted class component. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - - let node: Fiber = fiber; - while (node.tag !== HostRoot) { - if (node.tag === ClassComponent && node.type.childContextTypes != null) { - return node.stateNode.__reactInternalUnmaskedLegacyChildContext; +export function readContext( + context: ReactContext, + observedBits: void | number | boolean, +): T { + if (typeof observedBits !== 'number') { + if (observedBits === false) { + // Do not observe updates + observedBits = 0; + } else { + // Observe all updates + observedBits = maxSigned31BitInt; + } + } + + if (nextLastReader !== null) { + if (nextLastReader.context === context) { + // Fast path. The previous context has the same type. We can reuse + // the same node. + nextLastReader.observedBits |= observedBits; + } else { + // Append a new context item. + nextLastReader = nextLastReader.next = { + context: ((context: any): ReactContext), + observedBits, + next: null, + }; } - const parent = node.return; - invariant( - parent, - 'Found unexpected detached subtree parent. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - node = parent; + } else { + // This is the first reader in the list + nextFirstReader = nextLastReader = { + context: ((context: any): ReactContext), + observedBits, + next: null, + }; } - return node.stateNode.context; + + return isPrimaryRenderer ? context._currentValue : context._currentValue2; +} + +export function getContextChangedBits(context: ReactContext): number { + return isPrimaryRenderer ? context._changedBits : context._changedBits2; +} + +export function finishReadingContext(): ContextReader | null { + const list = nextFirstReader; + nextFirstReader = nextLastReader = null; + return list; } diff --git a/packages/react-reconciler/src/ReactFiberDispatcher.js b/packages/react-reconciler/src/ReactFiberDispatcher.js index f515457770168..53bf769ca96f1 100644 --- a/packages/react-reconciler/src/ReactFiberDispatcher.js +++ b/packages/react-reconciler/src/ReactFiberDispatcher.js @@ -7,7 +7,7 @@ * @flow */ -import {readContext} from './ReactFiberNewContext'; +import {readContext} from './ReactFiberContext'; export const Dispatcher = { readContext, diff --git a/packages/react-reconciler/src/ReactFiberLegacyContext.js b/packages/react-reconciler/src/ReactFiberLegacyContext.js new file mode 100644 index 0000000000000..39bde86da862a --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberLegacyContext.js @@ -0,0 +1,213 @@ +/** + * 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 type {Fiber} from './ReactFiber'; +import type {ReactContext} from 'shared/ReactTypes'; +import {isFiberMounted} from 'react-reconciler/reflection'; +import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork'; +import getComponentName from 'shared/getComponentName'; +import invariant from 'shared/invariant'; +import warning from 'shared/warning'; +import checkPropTypes from 'prop-types/checkPropTypes'; + +import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; +import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; +import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; +import {readContext, pushContext, popContext} from './ReactFiberContext'; +import maxSigned31BitInt from './maxSigned31BitInt'; +import {DidThrow, NoEffect} from 'shared/ReactTypeOfSideEffect'; + +let warnedAboutMissingGetChildContext; + +if (__DEV__) { + warnedAboutMissingGetChildContext = {}; +} +export const emptyContextObject = {}; +if (__DEV__) { + Object.freeze(emptyContextObject); +} + +export const LegacyContext: ReactContext = { + $$typeof: REACT_CONTEXT_TYPE, + _calculateChangedBits: null, + _defaultValue: emptyContextObject, + _currentValue: emptyContextObject, + _currentValue2: emptyContextObject, + _changedBits: 0, + _changedBits2: 0, + // These are circular + Provider: (null: any), + Consumer: (null: any), + unstable_read: (null: any), +}; + +LegacyContext.Provider = { + $$typeof: REACT_PROVIDER_TYPE, + _context: LegacyContext, +}; +LegacyContext.Consumer = LegacyContext; +LegacyContext.unstable_read = readContext.bind(null, LegacyContext); + +if (__DEV__) { + LegacyContext._currentRenderer = null; + LegacyContext._currentRenderer2 = null; +} + +export function calculateLegacyChildContext( + workInProgress: Fiber, + childContextTypes: Object, + unmaskedParentContext: Object, +): Object { + const instance = workInProgress.stateNode; + + let childContext; + // TODO (bvaughn) Replace this behavior with an invariant() in the future. + // It has only been added in Fiber to match the (unintentional) behavior in Stack. + if (typeof instance.getChildContext !== 'function') { + if (__DEV__) { + const componentName = getComponentName(workInProgress) || 'Unknown'; + + if (!warnedAboutMissingGetChildContext[componentName]) { + warnedAboutMissingGetChildContext[componentName] = true; + warning( + false, + '%s.childContextTypes is specified but there is no getChildContext() method ' + + 'on the instance. You can either define getChildContext() on %s or remove ' + + 'childContextTypes from it.', + componentName, + componentName, + ); + } + } + childContext = unmaskedParentContext; + } else { + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase('getChildContext'); + } + startPhaseTimer(workInProgress, 'getChildContext'); + childContext = instance.getChildContext(); + stopPhaseTimer(); + if (__DEV__) { + ReactDebugCurrentFiber.setCurrentPhase(null); + } + for (let contextKey in childContext) { + invariant( + contextKey in childContextTypes, + '%s.getChildContext(): key "%s" is not defined in childContextTypes.', + getComponentName(workInProgress) || 'Unknown', + contextKey, + ); + } + if (__DEV__) { + const name = getComponentName(workInProgress) || 'Unknown'; + checkPropTypes( + childContextTypes, + childContext, + 'child context', + name, + // In practice, there is one case in which we won't get a stack. It's when + // somebody calls unstable_renderSubtreeIntoContainer() and we process + // context from the parent component instance. The stack will be missing + // because it's outside of the reconciliation, and so the pointer has not + // been set. This is rare and doesn't matter. We'll also remove that API. + ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + ); + } + childContext = Object.assign({}, unmaskedParentContext, childContext); + } + + return childContext; +} + +export function pushLegacyContext( + workInProgress: Fiber, + childContextTypes: Object, + childContext: Object, + didChange: boolean, +): void { + const changedBits = didChange ? maxSigned31BitInt : 0; + pushContext(workInProgress, LegacyContext, childContext, changedBits); +} + +export function popLegacyContext(workInProgress: Fiber): void { + // Legacy context providers do not push their child context until the end of + // the render phase. If the render phase did not complete, the child context + // was never pushed. + if ((workInProgress.effectTag & DidThrow) === NoEffect) { + const childContextTypes = workInProgress.type.childContextTypes; + if (typeof childContextTypes === 'object' && childContextTypes != null) { + popContext(workInProgress, LegacyContext); + } + } +} + +export function pushRootLegacyContext( + workInProgress: Fiber, + rootContext: Object, + didChange: boolean, +): void { + const changedBits = didChange ? maxSigned31BitInt : 0; + pushContext(workInProgress, LegacyContext, rootContext, changedBits); +} + +export function popRootLegacyContext(workInProgress: Fiber): void { + popContext(workInProgress, LegacyContext); +} + +export function readUnmaskedLegacyContext(): Object { + return readContext(LegacyContext); +} + +export function maskLegacyContext( + workInProgress: Fiber, + unmaskedContext: Object, + contextTypes: Object, +): Object { + const maskedContext = {}; + for (let key in contextTypes) { + maskedContext[key] = unmaskedContext[key]; + } + + if (__DEV__) { + const name = getComponentName(workInProgress) || 'Unknown'; + checkPropTypes( + contextTypes, + maskedContext, + 'context', + name, + ReactDebugCurrentFiber.getCurrentFiberStackAddendum, + ); + } + return maskedContext; +} + +export function findCurrentUnmaskedContext(fiber: Fiber): Object { + // Currently this is only used with renderSubtreeIntoContainer; not sure if it + // makes sense elsewhere + invariant( + isFiberMounted(fiber) && fiber.tag === ClassComponent, + 'Expected subtree parent to be a mounted class component. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + let node: Fiber = fiber; + while (node.tag !== HostRoot) { + if (node.tag === ClassComponent && node.type.childContextTypes != null) { + return node.stateNode.__reactInternalUnmaskedLegacyChildContext; + } + const parent = node.return; + invariant( + parent, + 'Found unexpected detached subtree parent. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + node = parent; + } + return node.stateNode.context; +} diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js deleted file mode 100644 index 625168afca905..0000000000000 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ /dev/null @@ -1,293 +0,0 @@ -/** - * 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 type {ReactContext} from 'shared/ReactTypes'; -import type {Fiber} from './ReactFiber'; -import type {StackCursor} from './ReactFiberStack'; -import type {ExpirationTime} from './ReactFiberExpirationTime'; - -export type ContextReader = { - context: ReactContext, - observedBits: number, - next: ContextReader | null, -}; - -let nextFirstReader: ContextReader | null = null; -let nextLastReader: ContextReader | null = null; - -import warning from 'shared/warning'; - -import {isPrimaryRenderer} from './ReactFiberHostConfig'; -import {createCursor, push, pop} from './ReactFiberStack'; -import maxSigned31BitInt from './maxSigned31BitInt'; -import {NoWork} from './ReactFiberExpirationTime'; -import {ContextProvider} from 'shared/ReactTypeOfWork'; -import {LegacyContext} from './ReactFiberContext'; - -const valueCursor: StackCursor = createCursor(null); -const changedBitsCursor: StackCursor = createCursor(0); - -let rendererSigil; -if (__DEV__) { - // Use this to detect multiple renderers using the same context - rendererSigil = {}; -} - -export function pushProvider(providerFiber: Fiber): void { - const context: ReactContext = providerFiber.type._context; - const value = providerFiber.pendingProps.value; - const changedBits = providerFiber.stateNode; - pushContext(providerFiber, context, value, changedBits); -} - -export function pushContext( - workInProgress: Fiber, - context: ReactContext, - value: T, - changedBits: number, -): void { - if (isPrimaryRenderer) { - push(changedBitsCursor, context._changedBits, workInProgress); - push(valueCursor, context._currentValue, workInProgress); - - context._currentValue = value; - context._changedBits = changedBits; - if (__DEV__) { - warning( - context._currentRenderer === undefined || - context._currentRenderer === null || - context._currentRenderer === rendererSigil, - 'Detected multiple renderers concurrently rendering the ' + - 'same context provider. This is currently unsupported.', - ); - context._currentRenderer = rendererSigil; - } - } else { - push(changedBitsCursor, context._changedBits2, workInProgress); - push(valueCursor, context._currentValue2, workInProgress); - - context._currentValue2 = value; - context._changedBits2 = changedBits; - if (__DEV__) { - warning( - context._currentRenderer2 === undefined || - context._currentRenderer2 === null || - context._currentRenderer2 === rendererSigil, - 'Detected multiple renderers concurrently rendering the ' + - 'same context provider. This is currently unsupported.', - ); - context._currentRenderer2 = rendererSigil; - } - } -} - -export function popProvider(providerFiber: Fiber): void { - const context: ReactContext = providerFiber.type._context; - popContext(providerFiber, context); -} - -export function popContext( - workInProgress: Fiber, - context: ReactContext, -): void { - const changedBits = changedBitsCursor.current; - const currentValue = valueCursor.current; - - pop(valueCursor, workInProgress); - pop(changedBitsCursor, workInProgress); - - if (isPrimaryRenderer) { - context._currentValue = currentValue; - context._changedBits = changedBits; - } else { - context._currentValue2 = currentValue; - context._changedBits2 = changedBits; - } -} - -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 reader = fiber.firstContextReader; - if (reader !== null) { - do { - // Check if the context matches. - if ( - reader.context === context && - (reader.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; - } - reader = reader.next; - } while (reader !== 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; - } -} - -export function checkForPendingContext( - workInProgress: Fiber, - renderExpirationTime: ExpirationTime, -): boolean { - let reader = workInProgress.firstContextReader; - let hasPendingContext = false; - while (reader !== null) { - const context = reader.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 & reader.observedBits) !== 0) { - hasPendingContext = true; - } - } - reader = reader.next; - } - - return hasPendingContext || getContextChangedBits(LegacyContext) !== 0; -} - -export function prepareToReadContext(): void { - nextFirstReader = nextLastReader = null; -} - -export function readContext( - context: ReactContext, - observedBits: void | number | boolean, -): T { - if (typeof observedBits !== 'number') { - if (observedBits === false) { - // Do not observe updates - observedBits = 0; - } else { - // Observe all updates - observedBits = maxSigned31BitInt; - } - } - - if (nextLastReader !== null) { - if (nextLastReader.context === context) { - // Fast path. The previous context has the same type. We can reuse - // the same node. - nextLastReader.observedBits |= observedBits; - } else { - // Append a new context item. - nextLastReader = nextLastReader.next = { - context: ((context: any): ReactContext), - observedBits, - next: null, - }; - } - } else { - // This is the first reader in the list - nextFirstReader = nextLastReader = { - context: ((context: any): ReactContext), - observedBits, - next: null, - }; - } - - return isPrimaryRenderer ? context._currentValue : context._currentValue2; -} - -export function getContextChangedBits(context: ReactContext): number { - return isPrimaryRenderer ? context._changedBits : context._changedBits2; -} - -export function finishReadingContext(): ContextReader | null { - const list = nextFirstReader; - nextFirstReader = nextLastReader = null; - return list; -} diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 5aed379659d4e..4966455aeb6d6 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -33,7 +33,7 @@ import { findCurrentUnmaskedContext, emptyContextObject, maskLegacyContext, -} from './ReactFiberContext'; +} from './ReactFiberLegacyContext'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; import { diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 50828d2bd216d..b00acd7c76289 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -95,8 +95,8 @@ import { import {AsyncMode, ProfileMode} from './ReactTypeOfMode'; import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; -import {popRootLegacyContext} from './ReactFiberContext'; -import {popProvider} from './ReactFiberNewContext'; +import {popRootLegacyContext} from './ReactFiberLegacyContext'; +import {popProvider} from './ReactFiberContext'; import {popHostContext, popHostContainer} from './ReactFiberHostContext'; import { checkActualRenderTimeStackEmpty, diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 2a0f69becabc0..60b899efece91 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -46,8 +46,11 @@ import { import {logError} from './ReactFiberCommitWork'; import {Never, Sync, expirationTimeToMs} from './ReactFiberExpirationTime'; import {popHostContainer, popHostContext} from './ReactFiberHostContext'; -import {popLegacyContext, popRootLegacyContext} from './ReactFiberContext'; -import {popProvider} from './ReactFiberNewContext'; +import { + popLegacyContext, + popRootLegacyContext, +} from './ReactFiberLegacyContext'; +import {popProvider} from './ReactFiberContext'; import { resumeActualRenderTimerIfPaused, recordElapsedActualRenderTime,