diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 7e5a9dd3d61..978350cd55c 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 './ReactFiberContext'; 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 d2573168ec3..1bdd82332f8 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -69,21 +69,24 @@ import { import {pushHostContext, pushHostContainer} from './ReactFiberHostContext'; import { pushProvider, - getContextCurrentValue, - getContextChangedBits, -} from './ReactFiberNewContext'; + propagateContextChange, + checkForPendingContext, + readContext, + prepareToReadContext, + finishReadingContext, +} from './ReactFiberContext'; import { markActualRenderTimeStarted, stopBaseRenderTimerIfRunning, } from './ReactProfilerTimer'; import { - getMaskedContext, - getUnmaskedContext, - hasContextChanged as hasLegacyContextChanged, - pushContextProvider as pushLegacyContextProvider, - pushTopLevelContextObject, - invalidateContextProvider, -} from './ReactFiberContext'; + readUnmaskedLegacyContext, + maskLegacyContext, + emptyContextObject, + pushRootLegacyContext, + calculateLegacyChildContext, + pushLegacyContext, +} from './ReactFiberLegacyContext'; import { enterHydrationState, resetHydrationState, @@ -153,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); @@ -182,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); @@ -195,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); } @@ -211,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; @@ -236,34 +247,51 @@ 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. + 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(); + + let legacyContext; + const contextTypes = fn.contextTypes; + if (typeof contextTypes === 'object' && contextTypes !== null) { + const unmaskedContext = readUnmaskedLegacyContext(); + legacyContext = maskLegacyContext( + unmaskedContext, + unmaskedContext, + contextTypes, + ); } else { - if (workInProgress.memoizedProps === nextProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - // TODO: consider bringing fn.shouldComponentUpdate() back. - // It used to be here. + legacyContext = emptyContextObject; } - const unmaskedContext = getUnmaskedContext(workInProgress); - const context = getMaskedContext(workInProgress, unmaskedContext); - let nextChildren; 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(); + // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren); @@ -276,10 +304,34 @@ 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) { if (workInProgress.stateNode === null) { @@ -287,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, ); } @@ -303,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, ); } @@ -319,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; @@ -401,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; @@ -411,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); } @@ -500,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); @@ -576,8 +664,25 @@ 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; @@ -602,13 +707,15 @@ 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; + workInProgress.firstContextReader = finishReadingContext(); + if ( typeof value === 'object' && value !== null && @@ -635,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 { @@ -716,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); } @@ -737,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); } @@ -764,113 +879,18 @@ 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; 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); @@ -900,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); @@ -917,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); @@ -940,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); @@ -970,42 +990,15 @@ 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) { + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); + if (!hasPendingContext && 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; - } - // 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 +1012,8 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { ); } + prepareToReadContext(); + const newValue = readContext(context, newProps.unstable_observedBits); let newChildren; if (__DEV__) { ReactCurrentOwner.current = workInProgress; @@ -1028,6 +1023,7 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { } else { newChildren = render(newValue); } + workInProgress.firstContextReader = finishReadingContext(); // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; @@ -1098,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); @@ -1149,7 +1156,11 @@ function beginWork( renderExpirationTime, ); case FunctionalComponent: - return updateFunctionalComponent(current, workInProgress); + return updateFunctionalComponent( + current, + workInProgress, + renderExpirationTime, + ); case ClassComponent: return updateClassComponent( current, @@ -1175,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 a0ae9fb7823..99dc1f8424a 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -37,19 +37,13 @@ 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, scheduleWork, } from './ReactFiberScheduler'; +import {checkForPendingContext} from './ReactFiberContext'; const fakeInternalInstance = {}; const isArray = Array.isArray; @@ -231,7 +225,7 @@ function checkShouldComponentUpdate( newProps, oldState, newState, - newContext, + nextLegacyContext, ) { const instance = workInProgress.stateNode; const ctor = workInProgress.type; @@ -240,7 +234,7 @@ function checkShouldComponentUpdate( const shouldUpdate = instance.shouldComponentUpdate( newProps, newState, - newContext, + nextLegacyContext, ); stopPhaseTimer(); @@ -467,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__) { @@ -483,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 @@ -576,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; } @@ -616,15 +600,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(); @@ -649,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; @@ -659,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) { @@ -736,6 +720,7 @@ function mountClassInstance( function resumeMountClassInstance( workInProgress: Fiber, + nextLegacyContext: Object, renderExpirationTime: ExpirationTime, ): boolean { const ctor = workInProgress.type; @@ -746,8 +731,11 @@ function resumeMountClassInstance( instance.props = oldProps; const oldContext = instance.context; - const newUnmaskedContext = getUnmaskedContext(workInProgress); - const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = @@ -765,12 +753,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, ); } } @@ -793,7 +781,7 @@ function resumeMountClassInstance( if ( oldProps === newProps && oldState === newState && - !hasContextChanged() && + !hasPendingContext && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update @@ -821,7 +809,7 @@ function resumeMountClassInstance( newProps, oldState, newState, - newContext, + nextLegacyContext, ); if (shouldUpdate) { @@ -861,7 +849,7 @@ function resumeMountClassInstance( // if shouldComponentUpdate returns false. instance.props = newProps; instance.state = newState; - instance.context = newContext; + instance.context = nextLegacyContext; return shouldUpdate; } @@ -870,6 +858,7 @@ function resumeMountClassInstance( function updateClassInstance( current: Fiber, workInProgress: Fiber, + nextLegacyContext: Object, renderExpirationTime: ExpirationTime, ): boolean { const ctor = workInProgress.type; @@ -880,8 +869,11 @@ function updateClassInstance( instance.props = oldProps; const oldContext = instance.context; - const newUnmaskedContext = getUnmaskedContext(workInProgress); - const newContext = getMaskedContext(workInProgress, newUnmaskedContext); + + const hasPendingContext = checkForPendingContext( + workInProgress, + renderExpirationTime, + ); const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = @@ -899,12 +891,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, ); } } @@ -928,7 +920,7 @@ function updateClassInstance( if ( oldProps === newProps && oldState === newState && - !hasContextChanged() && + !hasPendingContext && !checkHasForceUpdateAfterProcessing() ) { // If an update was already in progress, we should schedule an Update @@ -969,7 +961,7 @@ function updateClassInstance( newProps, oldState, newState, - newContext, + nextLegacyContext, ); if (shouldUpdate) { @@ -982,10 +974,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 +1021,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/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index e8db8f19e84..d6d1c4142a4 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -62,10 +62,10 @@ import { } from './ReactFiberHostContext'; import {recordElapsedActualRenderTime} from './ReactProfilerTimer'; import { - popContextProvider as popLegacyContextProvider, - popTopLevelContextObject as popTopLevelLegacyContextObject, -} from './ReactFiberContext'; -import {popProvider} from './ReactFiberNewContext'; + popLegacyContext, + popRootLegacyContext, +} from './ReactFiberLegacyContext'; +import {popProvider} from './ReactFiberContext'; import { prepareToHydrateHostInstance, prepareToHydrateHostTextInstance, @@ -325,12 +325,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 1a1a30f35e0..7cca8c0a176 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -7,305 +7,287 @@ * @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 {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 {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 './ReactFiberLegacyContext'; -let warnedAboutMissingGetChildContext; - -if (__DEV__) { - warnedAboutMissingGetChildContext = {}; -} +const valueCursor: StackCursor = createCursor(null); +const changedBitsCursor: StackCursor = createCursor(0); -export const emptyContextObject = {}; +let rendererSigil; if (__DEV__) { - Object.freeze(emptyContextObject); + // Use this to detect multiple renderers using the same context + rendererSigil = {}; } -// 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; - -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; +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); } -function cacheContext( +export function pushContext( workInProgress: Fiber, - unmaskedContext: Object, - maskedContext: Object, + context: ReactContext, + value: T, + changedBits: number, ): void { - const instance = workInProgress.stateNode; - instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; - instance.__reactInternalMemoizedMaskedChildContext = maskedContext; -} - -function getMaskedContext( - workInProgress: Fiber, - unmaskedContext: 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 (isPrimaryRenderer) { + push(changedBitsCursor, context._changedBits, workInProgress); + push(valueCursor, context._currentValue, workInProgress); - if (__DEV__) { - const name = getComponentName(workInProgress) || 'Unknown'; - checkPropTypes( - contextTypes, - context, - 'context', - name, - ReactDebugCurrentFiber.getCurrentFiberStackAddendum, - ); - } + 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); - // 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); + 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; + } } - - return context; } -function hasContextChanged(): boolean { - return didPerformWorkStackCursor.current; +export function popProvider(providerFiber: Fiber): void { + const context: ReactContext = providerFiber.type._context; + popContext(providerFiber, context); } -function isContextConsumer(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.contextTypes != null; -} +export function popContext( + workInProgress: Fiber, + context: ReactContext, +): void { + const changedBits = changedBitsCursor.current; + const currentValue = valueCursor.current; -function isContextProvider(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.childContextTypes != null; -} + pop(valueCursor, workInProgress); + pop(changedBitsCursor, workInProgress); -function popContextProvider(fiber: Fiber): void { - if (!isContextProvider(fiber)) { - return; + if (isPrimaryRenderer) { + context._currentValue = currentValue; + context._changedBits = changedBits; + } else { + context._currentValue2 = currentValue; + context._changedBits2 = changedBits; } - - 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, +export function propagateContextChange( + workInProgress: Fiber, + context: ReactContext, + changedBits: number, + renderExpirationTime: ExpirationTime, ): 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; - - // 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'; + 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 (!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, - ); + 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; } } - return parentContext; + fiber = nextFiber; } - - 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 {...parentContext, ...childContext}; } -function pushContextProvider(workInProgress: Fiber): boolean { - if (!isContextProvider(workInProgress)) { - return false; +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; } - 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; + return hasPendingContext || getContextChangedBits(LegacyContext) !== 0; } -function invalidateContextProvider( - workInProgress: Fiber, - 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.', - ); +export function prepareToReadContext(): void { + nextFirstReader = nextLastReader = null; +} - 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 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; + } + } - // 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); + 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 { - pop(didPerformWorkStackCursor, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); + // This is the first reader in the list + nextFirstReader = nextLastReader = { + context: ((context: any): ReactContext), + observedBits, + next: null, + }; } -} -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.', - ); + return isPrimaryRenderer ? context._currentValue : context._currentValue2; +} - let node: Fiber = fiber; - while (node.tag !== HostRoot) { - if (isContextProvider(node)) { - return node.stateNode.__reactInternalMemoizedMergedChildContext; - } - 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; +export function getContextChangedBits(context: ReactContext): number { + return isPrimaryRenderer ? context._changedBits : context._changedBits2; } -export { - getUnmaskedContext, - cacheContext, - getMaskedContext, - hasContextChanged, - isContextConsumer, - isContextProvider, - popContextProvider, - popTopLevelContextObject, - pushTopLevelContextObject, - processChildContext, - pushContextProvider, - invalidateContextProvider, - findCurrentUnmaskedContext, -}; +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 new file mode 100644 index 00000000000..53bf769ca96 --- /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 './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 00000000000..39bde86da86 --- /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 4826412dded..00000000000 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ /dev/null @@ -1,106 +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 {Fiber} from './ReactFiber'; -import type {ReactContext} from 'shared/ReactTypes'; -import type {StackCursor} from './ReactFiberStack'; - -export type NewContext = { - pushProvider(providerFiber: Fiber): void, - popProvider(providerFiber: Fiber): void, - getContextCurrentValue(context: ReactContext): any, - getContextChangedBits(context: ReactContext): number, -}; - -import warning from 'shared/warning'; -import {isPrimaryRenderer} from './ReactFiberHostConfig'; -import {createCursor, push, pop} from './ReactFiberStack'; - -const providerCursor: StackCursor = createCursor(null); -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 = {}; -} - -function pushProvider(providerFiber: Fiber): void { - const context: ReactContext = providerFiber.type._context; - - if (isPrimaryRenderer) { - push(changedBitsCursor, context._changedBits, providerFiber); - push(valueCursor, context._currentValue, providerFiber); - push(providerCursor, providerFiber, providerFiber); - - context._currentValue = providerFiber.pendingProps.value; - context._changedBits = providerFiber.stateNode; - 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, providerFiber); - push(valueCursor, context._currentValue2, providerFiber); - push(providerCursor, providerFiber, providerFiber); - - context._currentValue2 = providerFiber.pendingProps.value; - context._changedBits2 = providerFiber.stateNode; - 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; - } - } -} - -function popProvider(providerFiber: Fiber): void { - const changedBits = changedBitsCursor.current; - const currentValue = valueCursor.current; - - pop(providerCursor, providerFiber); - pop(valueCursor, providerFiber); - pop(changedBitsCursor, providerFiber); - - const context: ReactContext = providerFiber.type._context; - if (isPrimaryRenderer) { - context._currentValue = currentValue; - context._changedBits = changedBits; - } else { - context._currentValue2 = currentValue; - context._changedBits2 = changedBits; - } -} - -function getContextCurrentValue(context: ReactContext): any { - return isPrimaryRenderer ? context._currentValue : context._currentValue2; -} - -function getContextChangedBits(context: ReactContext): number { - return isPrimaryRenderer ? context._changedBits : context._changedBits2; -} - -export { - pushProvider, - popProvider, - getContextCurrentValue, - getContextChangedBits, -}; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 245b1654e29..4966455aeb6 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,10 +31,9 @@ import warning from 'shared/warning'; import {getPublicInstance} from './ReactFiberHostConfig'; import { findCurrentUnmaskedContext, - isContextProvider, - processChildContext, emptyContextObject, -} from './ReactFiberContext'; + maskLegacyContext, +} from './ReactFiberLegacyContext'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; import { @@ -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 25a90af822e..b00acd7c762 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -95,11 +95,8 @@ 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 {popProvider} from './ReactFiberNewContext'; +import {popRootLegacyContext} from './ReactFiberLegacyContext'; +import {popProvider} from './ReactFiberContext'; import {popHostContext, popHostContainer} from './ReactFiberHostContext'; import { checkActualRenderTimeStackEmpty, @@ -135,6 +132,7 @@ import { commitAttachRef, commitDetachRef, } from './ReactFiberCommitWork'; +import {Dispatcher} from './ReactFiberDispatcher'; export type Deadline = { timeRemaining: () => number, @@ -279,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); @@ -985,6 +985,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 +1072,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/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 8bc5efa876b..60b899efece 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, @@ -46,10 +47,10 @@ 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 {popProvider} from './ReactFiberNewContext'; + popLegacyContext, + popRootLegacyContext, +} from './ReactFiberLegacyContext'; +import {popProvider} from './ReactFiberContext'; import { resumeActualRenderTimerIfPaused, recordElapsedActualRenderTime, @@ -71,7 +72,7 @@ function createRootErrorUpdate( fiber: Fiber, errorInfo: CapturedValue, expirationTime: ExpirationTime, -): Update { +): Update { const update = createUpdate(expirationTime); // Unmount the root by rendering null. update.tag = CaptureUpdate; @@ -147,7 +148,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 +326,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 +336,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 +378,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__/ReactContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactContext-test.internal.js new file mode 100644 index 00000000000..b73401b9b92 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactContext-test.internal.js @@ -0,0 +1,1537 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); + +let React = require('react'); +let ReactNoop; +let gen; + +describe('ReactContext', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + React = require('react'); + ReactNoop = require('react-noop-renderer'); + gen = require('random-seed'); + }); + + // function div(...children) { + // children = children.map(c => (typeof c === 'string' ? {text: c} : c)); + // return {type: 'div', children, prop: undefined}; + // } + + function Text(props) { + ReactNoop.yield(props.text); + return ; + } + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + // 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); + } + }, + ); + + function sharedContextTests(label, getConsumer) { + describe(`reading context with ${label}`, () => { + it('simple mount and update', () => { + const Context = React.createContext(1); + const Consumer = getConsumer(Context); + + const Indirection = React.Fragment; + + function App(props) { + return ( + + + + + {value => } + + + + + ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]); + + // Update + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]); + }); + + it('propagates through shouldComponentUpdate false', () => { + const Context = React.createContext(1); + const ContextConsumer = getConsumer(Context); + + function Provider(props) { + ReactNoop.yield('Provider'); + return ( + + {props.children} + + ); + } + + 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; + } + } + + 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} + + ); + } + + 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; + } + } + + 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 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; + } + } + + function App(props) { + return ( + + + + + + + + {value => } + + + + + + + + ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]); + + // 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 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'), + ]); + + // 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'), + ]); + }); + + 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) { + 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; + } + } + + 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: 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 BadRender() { + throw new Error('Bad render'); + } + + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return null; + } + return this.props.children; + } + } + + function App(props) { + 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); + + function Provider(props) { + return ( + + {props.children} + + ); + } + + function Foo() { + return ( + + {value => { + ReactNoop.yield('Foo'); + return ; + }} + + ); + } + + function Bar() { + return ( + + {value => { + ReactNoop.yield('Bar'); + return ; + }} + + ); + } + + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + 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); + + function Provider(props) { + return ( + + {props.children} + + ); + } + + function Foo(props) { + return ( + + {value => { + ReactNoop.yield('Foo'); + return ( + + + {props.children && props.children()} + + ); + }} + + ); + } + + function Bar(props) { + return ( + + {value => { + ReactNoop.yield('Bar'); + return ( + + + {props.children && props.children()} + + ); + }} + + ); + } + + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } + + function App(props) { + return ( + + + + {/* Use a render prop so we don't test constant elements. */} + {() => ( + + + {() => ( + + + + )} + + + )} + + + + ); + } + + 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 ( + + + {value => { + ReactNoop.yield('Consumer render prop'); + return (child = inst)} context={value} />; + }} + + + ); + } + + // Initial mount + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']); + expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]); + + child.setState({step: 1}); + expect(ReactNoop.flush()).toEqual(['Child']); + expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]); + }); + + it('consumer bails out if value is unchanged and something above bailed out', () => { + const Context = React.createContext(0); + const Consumer = getConsumer(Context); + + function renderChildValue(value) { + ReactNoop.yield('Consumer'); + return ; + } + + function ChildWithInlineRenderCallback() { + ReactNoop.yield('ChildWithInlineRenderCallback'); + // Note: we are intentionally passing an inline arrow. Don't refactor. + return {value => renderChildValue(value)}; + } + + function ChildWithCachedRenderCallback() { + ReactNoop.yield('ChildWithCachedRenderCallback'); + return {renderChildValue}; + } + + class PureIndirection extends React.PureComponent { + render() { + ReactNoop.yield('PureIndirection'); + return ( + + + + + ); + } + } + + class App extends React.Component { + render() { + ReactNoop.yield('App'); + return ( + + + + ); + } + } + + // 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} + + ); + } + } + + // 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(); + ReactNoop.flush(); + ReactNoop.render(); + ReactNoop.flush(); + ReactNoop.render(); + ReactNoop.flush(); + }); + + // 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); + + class App extends React.Component { + state = { + step: 0, + }; + + render() { + ReactNoop.yield('App'); + return ( + + + {this.state.step > 0 && } + + ); + } + } + + class StaticContent extends React.PureComponent { + render() { + return ( + + + + + + + ); + } + } + + class Indirection extends React.PureComponent { + render() { + return ( + + {value => { + ReactNoop.yield('Consumer'); + 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), + ]); + }); + }); + } + + describe('Context.Provider', () => { + it('warns if calculateChangedBits returns larger than a 31-bit integer', () => { + spyOnDev(console, 'error'); + + const Context = React.createContext( + 0, + (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int + ); + + ReactNoop.render(); + ReactNoop.flush(); + + // Update + ReactNoop.render(); + ReactNoop.flush(); + + 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 multiple renderers concurrently render the same context', () => { + spyOnDev(console, 'error'); + const Context = React.createContext(0); + + function Foo(props) { + ReactNoop.yield('Foo'); + return null; + } + + function App(props) { + return ( + + + + + ); + } + + ReactNoop.render(); + // Render past the Provider, but don't commit yet + ReactNoop.flushThrough(['Foo']); + + // Get a new copy of ReactNoop + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + + // 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', + ); + } + }); + + 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} + ); + } + + // 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 ; + } + + const children = ; + + 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 { + state = {value: 1}; + render() { + ReactNoop.yield('App'); + return ( + + {this.props.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')]); + }); + }); + + 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", + ); + } + }); + + it('can read other contexts inside consumer render prop', () => { + const FooContext = React.createContext(0); + const BarContext = React.createContext(0); + + function FooAndBar() { + return ( + + {foo => { + const bar = BarContext.unstable_read(); + return ; + }} + + ); + } + + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } + + function App(props) { + return ( + + + + + + + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Foo: 1, Bar: 1')]); + + // Update foo + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 1')]); + + // Update bar + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']); + expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 2')]); + }); + }); + + 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 ( + + {props.children} + + ); + } + + function FooAndBar() { + const {foo} = Context.unstable_read(0b001); + const {bar} = Context.unstable_read(0b010); + return ; + } + + function Baz() { + const {baz} = Context.unstable_read(0b100); + return ; + } + + class Indirection extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + return this.props.children; + } + } + + function App(props) { + return ( + + + + + + + + + + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1', 'Baz: 1']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 1, Bar: 1'), + span('Baz: 1'), + ]); + + // Update only foo + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2, Bar: 1'), + span('Baz: 1'), + ]); + + // Update only bar + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2, Bar: 2'), + span('Baz: 1'), + ]); + + // Update only baz + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Baz: 2']); + expect(ReactNoop.getChildren()).toEqual([ + span('Foo: 2, Bar: 2'), + span('Baz: 2'), + ]); + }); + }); + + describe('fuzz test', () => { + const Fragment = React.Fragment; + const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; + + const FLUSH_ALL = 'FLUSH_ALL'; + function flushAll() { + return { + type: FLUSH_ALL, + toString() { + return `flushAll()`; + }, + }; + } + + const FLUSH = 'FLUSH'; + function flush(unitsOfWork) { + return { + type: FLUSH, + unitsOfWork, + toString() { + return `flush(${unitsOfWork})`; + }, + }; + } + + const UPDATE = 'UPDATE'; + function update(key, value) { + return { + type: UPDATE, + key, + value, + toString() { + return `update('${key}', ${value})`; + }, + }; + } + + function randomInteger(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; + } + + function randomAction() { + switch (randomInteger(0, 3)) { + case 0: + return flushAll(); + case 1: + return flush(randomInteger(0, 500)); + case 2: + const key = contextKeys[randomInteger(0, contextKeys.length)]; + const value = randomInteger(1, 10); + return update(key, value); + default: + throw new Error('Switch statement should be exhaustive'); + } + } + + function randomActions(n) { + let actions = []; + for (let i = 0; i < n; i++) { + actions.push(randomAction()); + } + return actions; + } + + function ContextSimulator(maxDepth) { + const contexts = new Map( + contextKeys.map(key => { + const Context = React.createContext(0); + Context.displayName = 'Context' + key; + return [key, Context]; + }), + ); + + class ConsumerTree extends React.Component { + shouldComponentUpdate() { + return false; + } + render() { + if (this.props.depth >= this.props.maxDepth) { + return null; + } + const consumers = [0, 1, 2].map(i => { + const randomKey = + contextKeys[ + this.props.rand.intBetween(0, contextKeys.length - 1) + ]; + const Context = contexts.get(randomKey); + return ( + + {value => ( + + + + + )} + + ); + }); + return consumers; + } + } + + function Root(props) { + return contextKeys.reduceRight((children, key) => { + const Context = contexts.get(key); + const value = props.values[key]; + return {children}; + }, ); + } + + const initialValues = contextKeys.reduce( + (result, key, i) => ({...result, [key]: i + 1}), + {}, + ); + + function assertConsistentTree(expectedValues = {}) { + const children = ReactNoop.getChildren(); + children.forEach(child => { + const text = child.prop; + const key = text[0]; + const value = parseInt(text[2], 10); + const expectedValue = expectedValues[key]; + if (expectedValue === undefined) { + // If an expected value was not explicitly passed to this function, + // use the first occurrence. + expectedValues[key] = value; + } else if (value !== expectedValue) { + throw new Error( + `Inconsistent value! Expected: ${key}:${expectedValue}. Actual: ${text}`, + ); + } + }); + } + + function simulate(seed, actions) { + const rand = gen.create(seed); + let finalExpectedValues = initialValues; + function updateRoot() { + ReactNoop.render( + , + ); + } + updateRoot(); + + actions.forEach(action => { + switch (action.type) { + case FLUSH_ALL: + ReactNoop.flush(); + break; + case FLUSH: + ReactNoop.flushUnitsOfWork(action.unitsOfWork); + break; + case UPDATE: + finalExpectedValues = { + ...finalExpectedValues, + [action.key]: action.value, + }; + updateRoot(); + break; + default: + throw new Error('Switch statement should be exhaustive'); + } + assertConsistentTree(); + }); + + ReactNoop.flush(); + assertConsistentTree(finalExpectedValues); + } + + return {simulate}; + } + + it('hard-coded tests', () => { + const {simulate} = ContextSimulator(5); + simulate('randomSeed', [flush(3), update('A', 4)]); + }); + + it('generated tests', () => { + const {simulate} = ContextSimulator(5); + + const LIMIT = 100; + for (let i = 0; i < LIMIT; i++) { + const seed = Math.random() + .toString(36) + .substr(2, 5); + const actions = randomActions(5); + try { + simulate(seed, actions); + } catch (error) { + console.error(` +Context fuzz tester error! Copy and paste the following line into the test suite: + simulate('${seed}', ${actions.join(', ')}); +`); + throw error; + } + } + }); + }); +}); diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js deleted file mode 100644 index 95355e2a186..00000000000 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ /dev/null @@ -1,1342 +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. - * - * @emails react-core - */ - -'use strict'; - -let ReactFeatureFlags = require('shared/ReactFeatureFlags'); - -let React = require('react'); -let ReactNoop; -let gen; - -describe('ReactNewContext', () => { - beforeEach(() => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - React = require('react'); - ReactNoop = require('react-noop-renderer'); - gen = require('random-seed'); - }); - - // function div(...children) { - // children = children.map(c => (typeof c === 'string' ? {text: c} : c)); - // return {type: 'div', children, prop: undefined}; - // } - - 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')]); - }); - - it('propagates through shouldComponentUpdate false', () => { - const Context = React.createContext(1); - - function Provider(props) { - ReactNoop.yield('Provider'); - return ( - - {props.children} - - ); - } - - 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; - } - } - - 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); - - function Provider(props) { - ReactNoop.yield('Provider'); - return ( - - {props.children} - - ); - } - - 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; - } - } - - 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 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); - - function Provider(props) { - return ( - - {contextValue => ( - // Multiply previous context value by 2, unless prop overrides - - {props.children} - - )} - - ); - } - - function Consumer(props) { - return ( - - {value => } - - ); - } - - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - return this.props.children; - } - } - - function App(props) { - return ( - - - - - - - - - - - - - - ); - } - - ReactNoop.render(); - ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]); - - // 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('multiple consumers in different branches', () => { - const Context = React.createContext(1); - - function Provider(props) { - return ( - - {contextValue => ( - // Multiply previous context value by 2, unless prop overrides - - {props.children} - - )} - - ); - } - - function Consumer(props) { - return ( - - {value => } - - ); - } - - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - return this.props.children; - } - } - - function App(props) { - return ( - - - - - - - - - - - - - ); - } - - 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'), - ]); - }); - - it('compares context values with Object.is semantics', () => { - const Context = React.createContext(1); - - function Provider(props) { - ReactNoop.yield('Provider'); - return ( - - {props.children} - - ); - } - - 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; - } - } - - 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: 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'); - - function Consumer(props) { - return ( - - {value => } - - ); - } - - function BadRender() { - throw new Error('Bad render'); - } - - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - render() { - if (this.state.error) { - return null; - } - return this.props.children; - } - } - - function App(props) { - 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; - }); - - function Provider(props) { - return ( - - {props.children} - - ); - } - - function Foo() { - return ( - - {value => { - ReactNoop.yield('Foo'); - return ; - }} - - ); - } - - function Bar() { - return ( - - {value => { - ReactNoop.yield('Bar'); - return ; - }} - - ); - } - - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - 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; - }); - - function Provider(props) { - return ( - - {props.children} - - ); - } - - function Foo(props) { - return ( - - {value => { - ReactNoop.yield('Foo'); - return ( - - - {props.children && props.children()} - - ); - }} - - ); - } - - function Bar(props) { - return ( - - {value => { - ReactNoop.yield('Bar'); - return ( - - - {props.children && props.children()} - - ); - }} - - ); - } - - class Indirection extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - return this.props.children; - } - } - - function App(props) { - return ( - - - - {/* Use a render prop so we don't test constant elements. */} - {() => ( - - - {() => ( - - - - )} - - - )} - - - - ); - } - - 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('warns if calculateChangedBits returns larger than a 31-bit integer', () => { - spyOnDev(console, 'error'); - - const Context = React.createContext( - 0, - (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int - ); - - ReactNoop.render(); - ReactNoop.flush(); - - // Update - ReactNoop.render(); - ReactNoop.flush(); - - 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 multiple renderers concurrently render the same context', () => { - spyOnDev(console, 'error'); - const Context = React.createContext(0); - - function Foo(props) { - ReactNoop.yield('Foo'); - return null; - } - - function App(props) { - return ( - - - - - ); - } - - ReactNoop.render(); - // Render past the Provider, but don't commit yet - ReactNoop.flushThrough(['Foo']); - - // Get a new copy of ReactNoop - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - React = require('react'); - ReactNoop = require('react-noop-renderer'); - - // 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', - ); - } - }); - - 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("does not re-render if there's an update in a child", () => { - const Context = React.createContext(0); - - let child; - class Child extends React.Component { - state = {step: 0}; - render() { - ReactNoop.yield('Child'); - return ( - - ); - } - } - - function App(props) { - return ( - - - {value => { - ReactNoop.yield('Consumer render prop'); - return (child = inst)} context={value} />; - }} - - - ); - } - - // Initial mount - ReactNoop.render(); - expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']); - expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]); - - child.setState({step: 1}); - expect(ReactNoop.flush()).toEqual(['Child']); - expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]); - }); - - it('provider bails out if children and value are unchanged (like sCU)', () => { - const Context = React.createContext(0); - - function Child() { - ReactNoop.yield('Child'); - return ; - } - - const children = ; - - function App(props) { - ReactNoop.yield('App'); - return ( - {children} - ); - } - - // 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 ; - } - - const children = ; - - 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 { - state = {value: 1}; - render() { - ReactNoop.yield('App'); - return ( - - {this.props.children} - - ); - } - } - - const legacyProviderRef = React.createRef(); - const appRef = React.createRef(); - - // Initial mount - ReactNoop.render( - - - {children} - - , - ); - expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - - // Update App with same value (should bail out) - appRef.current.setState({value: 1}); - expect(ReactNoop.flush()).toEqual(['App']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - - // Update LegacyProvider (should not bail out) - legacyProviderRef.current.setState({value: 1}); - expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - - // Update App with same value (should bail out) - appRef.current.setState({value: 1}); - expect(ReactNoop.flush()).toEqual(['App']); - expect(ReactNoop.getChildren()).toEqual([span('Child')]); - }); - - it('consumer bails out if value is unchanged and something above bailed out', () => { - const Context = React.createContext(0); - - function renderChildValue(value) { - ReactNoop.yield('Consumer'); - return ; - } - - function ChildWithInlineRenderCallback() { - ReactNoop.yield('ChildWithInlineRenderCallback'); - // Note: we are intentionally passing an inline arrow. Don't refactor. - return ( - {value => renderChildValue(value)} - ); - } - - function ChildWithCachedRenderCallback() { - ReactNoop.yield('ChildWithCachedRenderCallback'); - return {renderChildValue}; - } - - class PureIndirection extends React.PureComponent { - render() { - ReactNoop.yield('PureIndirection'); - return ( - - - - - ); - } - } - - class App extends React.Component { - render() { - ReactNoop.yield('App'); - return ( - - - - ); - } - } - - // 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); - - class App extends React.Component { - state = { - text: 'hello', - }; - - renderConsumer = context => { - ReactNoop.yield('App#renderConsumer'); - return ; - }; - - render() { - ReactNoop.yield('App'); - return ( - - {this.renderConsumer} - - ); - } - } - - // 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); - - 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(); - ReactNoop.flush(); - ReactNoop.render(); - ReactNoop.flush(); - ReactNoop.render(); - ReactNoop.flush(); - }); - - // This is a regression case for https://github.com/facebook/react/issues/12686 - it('does not skip some siblings', () => { - const Context = React.createContext(0); - - class App extends React.Component { - state = { - step: 0, - }; - - render() { - ReactNoop.yield('App'); - return ( - - - {this.state.step > 0 && } - - ); - } - } - - class StaticContent extends React.PureComponent { - render() { - return ( - - - - - - - ); - } - } - - class Indirection extends React.PureComponent { - render() { - return ; - } - } - - function Consumer() { - return ( - - {value => { - ReactNoop.yield('Consumer'); - 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), - ]); - }); - - describe('fuzz test', () => { - const Fragment = React.Fragment; - const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; - - const FLUSH_ALL = 'FLUSH_ALL'; - function flushAll() { - return { - type: FLUSH_ALL, - toString() { - return `flushAll()`; - }, - }; - } - - const FLUSH = 'FLUSH'; - function flush(unitsOfWork) { - return { - type: FLUSH, - unitsOfWork, - toString() { - return `flush(${unitsOfWork})`; - }, - }; - } - - const UPDATE = 'UPDATE'; - function update(key, value) { - return { - type: UPDATE, - key, - value, - toString() { - return `update('${key}', ${value})`; - }, - }; - } - - function randomInteger(min, max) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min)) + min; - } - - function randomAction() { - switch (randomInteger(0, 3)) { - case 0: - return flushAll(); - case 1: - return flush(randomInteger(0, 500)); - case 2: - const key = contextKeys[randomInteger(0, contextKeys.length)]; - const value = randomInteger(1, 10); - return update(key, value); - default: - throw new Error('Switch statement should be exhaustive'); - } - } - - function randomActions(n) { - let actions = []; - for (let i = 0; i < n; i++) { - actions.push(randomAction()); - } - return actions; - } - - function ContextSimulator(maxDepth) { - const contexts = new Map( - contextKeys.map(key => { - const Context = React.createContext(0); - Context.displayName = 'Context' + key; - return [key, Context]; - }), - ); - - class ConsumerTree extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - if (this.props.depth >= this.props.maxDepth) { - return null; - } - const consumers = [0, 1, 2].map(i => { - const randomKey = - contextKeys[ - this.props.rand.intBetween(0, contextKeys.length - 1) - ]; - const Context = contexts.get(randomKey); - return ( - - {value => ( - - - - - )} - - ); - }); - return consumers; - } - } - - function Root(props) { - return contextKeys.reduceRight((children, key) => { - const Context = contexts.get(key); - const value = props.values[key]; - return {children}; - }, ); - } - - const initialValues = contextKeys.reduce( - (result, key, i) => ({...result, [key]: i + 1}), - {}, - ); - - function assertConsistentTree(expectedValues = {}) { - const children = ReactNoop.getChildren(); - children.forEach(child => { - const text = child.prop; - const key = text[0]; - const value = parseInt(text[2], 10); - const expectedValue = expectedValues[key]; - if (expectedValue === undefined) { - // If an expected value was not explicitly passed to this function, - // use the first occurrence. - expectedValues[key] = value; - } else if (value !== expectedValue) { - throw new Error( - `Inconsistent value! Expected: ${key}:${expectedValue}. Actual: ${text}`, - ); - } - }); - } - - function simulate(seed, actions) { - const rand = gen.create(seed); - let finalExpectedValues = initialValues; - function updateRoot() { - ReactNoop.render( - , - ); - } - updateRoot(); - - actions.forEach(action => { - switch (action.type) { - case FLUSH_ALL: - ReactNoop.flush(); - break; - case FLUSH: - ReactNoop.flushUnitsOfWork(action.unitsOfWork); - break; - case UPDATE: - finalExpectedValues = { - ...finalExpectedValues, - [action.key]: action.value, - }; - updateRoot(); - break; - default: - throw new Error('Switch statement should be exhaustive'); - } - assertConsistentTree(); - }); - - ReactNoop.flush(); - assertConsistentTree(finalExpectedValues); - } - - return {simulate}; - } - - it('hard-coded tests', () => { - const {simulate} = ContextSimulator(5); - simulate('randomSeed', [flush(3), update('A', 4)]); - }); - - it('generated tests', () => { - const {simulate} = ContextSimulator(5); - - const LIMIT = 100; - for (let i = 0; i < LIMIT; i++) { - const seed = Math.random() - .toString(36) - .substr(2, 5); - const actions = randomActions(5); - try { - simulate(seed, actions); - } catch (error) { - console.error(` -Context fuzz tester error! Copy and paste the following line into the test suite: - simulate('${seed}', ${actions.join(', ')}); -`); - throw error; - } - } - }); - }); -}); diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 4c1b06bfed5..8a972139b6e 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 72ed4e2eb84..89cd104ca6a 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/ReactTypeOfSideEffect.js b/packages/shared/ReactTypeOfSideEffect.js index 27d6aa6090e..b9329fc73ea 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; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index ac4eed362d0..bff9413c828 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,