From 1b9e62129e228faf7f18578ee62e0eea151e0222 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 9 Dec 2017 01:39:14 -0800 Subject: [PATCH 01/17] New context API Introduces a declarative context API that propagates updates even when shouldComponentUpdate returns false. --- .../react-reconciler/src/ReactChildFiber.js | 214 +++++++++++- packages/react-reconciler/src/ReactFiber.js | 42 ++- .../src/ReactFiberBeginWork.js | 236 +++++++++++++- .../src/ReactFiberCompleteWork.js | 10 + .../src/__tests__/ReactNewContext-test.js | 306 ++++++++++++++++++ packages/react/src/React.js | 4 + packages/shared/ReactContext.js | 46 +++ packages/shared/ReactSymbols.js | 6 + packages/shared/ReactTypeOfWork.js | 18 +- packages/shared/ReactTypes.js | 28 +- 10 files changed, 889 insertions(+), 21 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/ReactNewContext-test.js create mode 100644 packages/shared/ReactContext.js diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 1722bf81263df..220269fc499d8 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -8,7 +8,11 @@ */ import type {ReactElement} from 'shared/ReactElementType'; -import type {ReactPortal} from 'shared/ReactTypes'; +import type { + ReactPortal, + ReactProvider, + ReactConsumer, +} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime'; @@ -18,6 +22,8 @@ import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, + REACT_PROVIDER_TYPE, + REACT_CONSUMER_TYPE, } from 'shared/ReactSymbols'; import { FunctionalComponent, @@ -25,6 +31,8 @@ import { HostText, HostPortal, Fragment, + ProviderComponent, + ConsumerComponent, } from 'shared/ReactTypeOfWork'; import emptyObject from 'fbjs/lib/emptyObject'; import invariant from 'fbjs/lib/invariant'; @@ -36,6 +44,8 @@ import { createFiberFromFragment, createFiberFromText, createFiberFromPortal, + createFiberFromProvider, + createFiberFromConsumer, } from './ReactFiber'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; @@ -413,6 +423,52 @@ function ChildReconciler(shouldTrackSideEffects) { } } + function updateProviderComponent( + returnFiber: Fiber, + current: Fiber | null, + provider: ReactProvider, + expirationTime: ExpirationTime, + ) { + if (current !== null && current.type === provider.context) { + // Move based on index + const existing = useFiber(current, provider, expirationTime); + existing.return = returnFiber; + return existing; + } else { + // Insert + const created = createFiberFromProvider( + provider, + returnFiber.internalContextTag, + expirationTime, + ); + created.return = returnFiber; + return created; + } + } + + function updateConsumerComponent( + returnFiber: Fiber, + current: Fiber | null, + consumer: ReactConsumer, + expirationTime: ExpirationTime, + ) { + if (current !== null && current.type === consumer.context) { + // Move based on index + const existing = useFiber(current, consumer, expirationTime); + existing.return = returnFiber; + return existing; + } else { + // Insert + const created = createFiberFromConsumer( + consumer, + returnFiber.internalContextTag, + expirationTime, + ); + created.return = returnFiber; + return created; + } + } + function createChild( returnFiber: Fiber, newChild: any, @@ -452,6 +508,24 @@ function ChildReconciler(shouldTrackSideEffects) { created.return = returnFiber; return created; } + case REACT_PROVIDER_TYPE: { + const created = createFiberFromProvider( + newChild, + returnFiber.internalContextTag, + expirationTime, + ); + created.return = returnFiber; + return created; + } + case REACT_CONSUMER_TYPE: { + const created = createFiberFromConsumer( + newChild, + returnFiber.internalContextTag, + expirationTime, + ); + created.return = returnFiber; + return created; + } } if (isArray(newChild) || getIteratorFn(newChild)) { @@ -537,6 +611,30 @@ function ChildReconciler(shouldTrackSideEffects) { return null; } } + case REACT_PROVIDER_TYPE: { + if (newChild.key === key) { + return updateProviderComponent( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); + } else { + return null; + } + } + case REACT_CONSUMER_TYPE: { + if (newChild.key === key) { + return updateConsumerComponent( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); + } else { + return null; + } + } } if (isArray(newChild) || getIteratorFn(newChild)) { @@ -619,6 +717,30 @@ function ChildReconciler(shouldTrackSideEffects) { expirationTime, ); } + case REACT_PROVIDER_TYPE: { + const matchedFiber = + existingChildren.get( + newChild.key === null ? newIdx : newChild.key, + ) || null; + return updateProviderComponent( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); + } + case REACT_CONSUMER_TYPE: { + const matchedFiber = + existingChildren.get( + newChild.key === null ? newIdx : newChild.key, + ) || null; + return updateConsumerComponent( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); + } } if (isArray(newChild) || getIteratorFn(newChild)) { @@ -1159,6 +1281,78 @@ function ChildReconciler(shouldTrackSideEffects) { return created; } + function reconcileSingleProvider( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + provider: ReactProvider, + expirationTime: ExpirationTime, + ): Fiber { + const key = provider.key; + let child = currentFirstChild; + while (child !== null) { + // TODO: If key === null and child.key === null, then this only applies to + // the first item in the list. + if (child.key === key && child.type === provider.context) { + if (child.tag === ProviderComponent) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, provider, expirationTime); + existing.return = returnFiber; + return existing; + } else { + deleteRemainingChildren(returnFiber, child); + break; + } + } else { + deleteChild(returnFiber, child); + } + child = child.sibling; + } + + const created = createFiberFromProvider( + provider, + returnFiber.internalContextTag, + expirationTime, + ); + created.return = returnFiber; + return created; + } + + function reconcileSingleConsumer( + returnFiber: Fiber, + currentFirstChild: Fiber | null, + consumer: ReactConsumer, + expirationTime: ExpirationTime, + ): Fiber { + const key = consumer.key; + let child = currentFirstChild; + while (child !== null) { + // TODO: If key === null and child.key === null, then this only applies to + // the first item in the list. + if (child.key === key && child.type === consumer.context) { + if (child.tag === ConsumerComponent) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, consumer, expirationTime); + existing.return = returnFiber; + return existing; + } else { + deleteRemainingChildren(returnFiber, child); + break; + } + } else { + deleteChild(returnFiber, child); + } + child = child.sibling; + } + + const created = createFiberFromConsumer( + consumer, + returnFiber.internalContextTag, + expirationTime, + ); + created.return = returnFiber; + return created; + } + // This API will tag the children with the side-effect of the reconciliation // itself. They will be added to the side-effect list as we pass through the // children and the parent. @@ -1208,6 +1402,24 @@ function ChildReconciler(shouldTrackSideEffects) { expirationTime, ), ); + case REACT_PROVIDER_TYPE: + return placeSingleChild( + reconcileSingleProvider( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ), + ); + case REACT_CONSUMER_TYPE: + return placeSingleChild( + reconcileSingleConsumer( + returnFiber, + currentFirstChild, + newChild, + expirationTime, + ), + ); } } diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index d36433099c982..356f5e230a592 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -7,7 +7,11 @@ */ import type {ReactElement, Source} from 'shared/ReactElementType'; -import type {ReactFragment, ReactPortal} from 'shared/ReactTypes'; +import type { + ReactPortal, + ReactProvider, + ReactConsumer, +} from 'shared/ReactTypes'; import type {TypeOfWork} from 'shared/ReactTypeOfWork'; import type {TypeOfInternalContext} from './ReactTypeOfInternalContext'; import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect'; @@ -27,6 +31,8 @@ import { ReturnComponent, Fragment, Mode, + ProviderComponent, + ConsumerComponent, } from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; @@ -471,3 +477,37 @@ export function createFiberFromPortal( }; return fiber; } + +export function createFiberFromProvider( + provider: ReactProvider, + internalContextTag: TypeOfInternalContext, + expirationTime: ExpirationTime, +): Fiber { + const pendingProps = provider; + const fiber = createFiber( + ProviderComponent, + pendingProps, + provider.key, + internalContextTag, + ); + fiber.expirationTime = expirationTime; + fiber.type = provider.context; + return fiber; +} + +export function createFiberFromConsumer( + consumer: ReactConsumer, + internalContextTag: TypeOfInternalContext, + expirationTime: ExpirationTime, +): Fiber { + const pendingProps = consumer; + const fiber = createFiber( + ConsumerComponent, + pendingProps, + consumer.key, + internalContextTag, + ); + fiber.expirationTime = expirationTime; + fiber.type = consumer.context; + return fiber; +} diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index d0569a6b4ad90..5edc544026605 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -8,6 +8,11 @@ */ import type {HostConfig} from 'react-reconciler'; +import type { + ReactProvider, + ReactConsumer, + ReactContext, +} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {HostContext} from './ReactFiberHostContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; @@ -27,6 +32,8 @@ import { ReturnComponent, Fragment, Mode, + ProviderComponent, + ConsumerComponent, } from 'shared/ReactTypeOfWork'; import { PerformedWork, @@ -53,8 +60,8 @@ import {processUpdateQueue} from './ReactFiberUpdateQueue'; import { getMaskedContext, getUnmaskedContext, - hasContextChanged, - pushContextProvider, + hasContextChanged as hasLegacyContextChanged, + pushContextProvider as pushLegacyContextProvider, pushTopLevelContextObject, invalidateContextProvider, } from './ReactFiberContext'; @@ -147,13 +154,10 @@ export default function( function updateFragment(current, workInProgress) { const nextChildren = workInProgress.pendingProps; - if (hasContextChanged()) { + 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 - ) { + } else if (workInProgress.memoizedProps === nextChildren) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } reconcileChildren(current, workInProgress, nextChildren); @@ -163,7 +167,7 @@ export default function( function updateMode(current, workInProgress) { const nextChildren = workInProgress.pendingProps.children; - if (hasContextChanged()) { + 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 ( @@ -189,7 +193,7 @@ export default function( const fn = workInProgress.type; const nextProps = workInProgress.pendingProps; - if (hasContextChanged()) { + 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 { @@ -228,7 +232,7 @@ export default function( // 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 = pushContextProvider(workInProgress); + const hasContext = pushLegacyContextProvider(workInProgress); let shouldUpdate; if (current === null) { @@ -397,7 +401,7 @@ export default function( const nextProps = workInProgress.pendingProps; const prevProps = current !== null ? current.memoizedProps : null; - if (hasContextChanged()) { + 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) { @@ -492,7 +496,8 @@ export default function( if ( typeof value === 'object' && value !== null && - typeof value.render === 'function' + typeof value.render === 'function' && + value.$$typeof === undefined ) { const Component = workInProgress.type; @@ -521,7 +526,7 @@ export default function( // 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 = pushContextProvider(workInProgress); + const hasContext = pushLegacyContextProvider(workInProgress); adoptClassInstance(workInProgress, value); mountClassInstance(workInProgress, renderExpirationTime); return finishClassComponent(current, workInProgress, true, hasContext); @@ -587,7 +592,7 @@ export default function( function updateCallComponent(current, workInProgress, renderExpirationTime) { let nextProps = workInProgress.pendingProps; - if (hasContextChanged()) { + 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) { @@ -630,7 +635,7 @@ export default function( ) { pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); const nextChildren = workInProgress.pendingProps; - if (hasContextChanged()) { + 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) { @@ -657,6 +662,188 @@ export default function( return workInProgress.child; } + function pushContextProvider(workInProgress) { + const context: ReactContext = workInProgress.type; + // Store a reference to the previous provider + // TODO: Only need to do this on mount + workInProgress.stateNode = context.lastProvider; + // Push this onto the list of providers. We'll pop in the complete phase. + context.lastProvider = workInProgress; + } + + function propagateContextChange( + workInProgress: Fiber, + context: ReactContext, + renderExpirationTime: ExpirationTime, + ): void { + let fiber = workInProgress.child; + while (fiber !== null) { + let nextFiber; + // Visit this fiber. + switch (fiber.tag) { + case ConsumerComponent: + // Check if the context matches. + if (fiber.type === context) { + // 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 ProviderComponent: + // Don't scan deeper if this is a matching provider + nextFiber = fiber.type === context ? 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) { + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; + } + } + fiber = nextFiber; + } + } + + function updateProviderComponent( + current, + workInProgress, + renderExpirationTime, + ) { + const context: ReactContext = workInProgress.type; + + const newProvider: ReactProvider = workInProgress.pendingProps; + const oldProvider: ReactProvider | null = workInProgress.memoizedProps; + + pushContextProvider(workInProgress); + + 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 (oldProvider === newProvider) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + workInProgress.memoizedProps = newProvider; + + const newValue = newProvider.value; + const oldValue = oldProvider !== null ? oldProvider.value : null; + // TODO: Use Object.is instead of === + if (newValue !== oldValue) { + propagateContextChange(workInProgress, context, renderExpirationTime); + } + + if (oldProvider !== null && oldProvider.children === newProvider.children) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + const newChildren = newProvider.children; + reconcileChildren(current, workInProgress, newChildren); + return workInProgress.child; + } + + function updateConsumerComponent( + current, + workInProgress, + renderExpirationTime, + ) { + const context: ReactContext = workInProgress.type; + + const newConsumer: ReactConsumer = workInProgress.pendingProps; + const oldConsumer: ReactConsumer = workInProgress.memoizedProps; + + // Get the nearest ancestor provider. + const providerFiber: Fiber | null = context.lastProvider; + + let newValue; + if (providerFiber === null) { + // This is a detached consumer (has no provider). Use the default + // context value. + newValue = context.defaultValue; + } else { + const provider = providerFiber.pendingProps; + invariant( + provider, + 'Provider should have pending props. This error is likely caused by ' + + 'a bug in React. Please file an issue.', + ); + newValue = provider.value; + } + // The old context value is stored on the consumer object. We can't use the + // provider's memoizedProps because those have already been updated by the + // time we get here, in the provider's begin phase. + const oldValue = oldConsumer !== null ? oldConsumer.memoizedValue : null; + newConsumer.memoizedValue = newValue; + + // Context change propagation stops at matching consumers, for time-slicing. + // Continue the propagation here. + // TODO: Use Object.is instead of === + if (newValue !== oldValue) { + propagateContextChange(workInProgress, context, renderExpirationTime); + // Because the context value has changed, do not bail out, even if the + // consumer objects match. + } else 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 (newConsumer === oldConsumer) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + + const newChildren = newConsumer.render(newValue); + reconcileChildren(current, workInProgress, newChildren); + return workInProgress.child; + } + /* function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) { let child = firstChild; @@ -710,7 +897,7 @@ export default function( pushHostRootContext(workInProgress); break; case ClassComponent: - pushContextProvider(workInProgress); + pushLegacyContextProvider(workInProgress); break; case HostPortal: pushHostContainer( @@ -718,6 +905,9 @@ export default function( workInProgress.stateNode.containerInfo, ); break; + case ProviderComponent: + pushContextProvider(workInProgress); + break; } // TODO: What if this is currently in progress? // How can that happen? How is this not being cloned? @@ -796,6 +986,18 @@ export default function( return updateFragment(current, workInProgress); case Mode: return updateMode(current, workInProgress); + case ProviderComponent: + return updateProviderComponent( + current, + workInProgress, + renderExpirationTime, + ); + case ConsumerComponent: + return updateConsumerComponent( + current, + workInProgress, + renderExpirationTime, + ); default: invariant( false, @@ -813,7 +1015,7 @@ export default function( // Push context providers here to avoid a push/pop context mismatch. switch (workInProgress.tag) { case ClassComponent: - pushContextProvider(workInProgress); + pushLegacyContextProvider(workInProgress); break; case HostRoot: pushHostRootContext(workInProgress); diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index d63f70ebbbb74..f3302d20dd68e 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -30,6 +30,8 @@ import { CallComponent, CallHandlerPhase, ReturnComponent, + ProviderComponent, + ConsumerComponent, Fragment, Mode, } from 'shared/ReactTypeOfWork'; @@ -583,6 +585,14 @@ export default function( popHostContainer(workInProgress); updateHostContainer(workInProgress); return null; + case ProviderComponent: + const context = workInProgress.type; + // Pop provider fiber + const lastProvider = workInProgress.stateNode; + context.lastProvider = lastProvider; + return null; + case ConsumerComponent: + return null; // Error cases case IndeterminateComponent: invariant( diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js new file mode 100644 index 0000000000000..607b8385f7c7b --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js @@ -0,0 +1,306 @@ +/** + * 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 React; +let ReactNoop; + +describe('ReactNewContext', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + }); + + // 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 Provider(props) { + return Context.provide(props.value, props.children); + } + + function Consumer(props) { + return Context.consume(value => { + return ; + }); + } + + 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 Context.provide(props.value, props.children); + } + + function Consumer(props) { + ReactNoop.yield('Consumer'); + return Context.consume(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 Context.provide(props.value, props.children); + } + + function Consumer(props) { + ReactNoop.yield('Consumer'); + return Context.consume(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 Context.consume(contextValue => + // Multiply previous context value by 2, unless prop overrides + Context.provide(props.value || contextValue * 2, props.children), + ); + } + + function Consumer(props) { + return Context.consume(value => { + return ; + }); + } + + 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('multiple consumers in different branches', () => { + const Context = React.createContext(1); + + function Provider(props) { + return Context.consume(contextValue => + // Multiply previous context value by 2, unless prop overrides + Context.provide(props.value || contextValue * 2, props.children), + ); + } + + function Consumer(props) { + return Context.consume(value => { + return ; + }); + } + + 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'), + ]); + }); +}); diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 81247eba6f0d4..c987a5f16e4f9 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -18,6 +18,7 @@ import { cloneElement, isValidElement, } from './ReactElement'; +import {createContext} from 'shared/ReactContext'; import { createElementWithValidation, createFactoryWithValidation, @@ -41,6 +42,9 @@ const React = { Fragment: REACT_FRAGMENT_TYPE, StrictMode: REACT_STRICT_MODE_TYPE, + // TODO: Feature flag + createContext, + createElement: __DEV__ ? createElementWithValidation : createElement, cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement, createFactory: __DEV__ ? createFactoryWithValidation : createFactory, diff --git a/packages/shared/ReactContext.js b/packages/shared/ReactContext.js new file mode 100644 index 0000000000000..63a41d2857442 --- /dev/null +++ b/packages/shared/ReactContext.js @@ -0,0 +1,46 @@ +/** + * 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 {REACT_PROVIDER_TYPE, REACT_CONSUMER_TYPE} from 'shared/ReactSymbols'; + +import type { + ReactContext, + ReactConsumer, + ReactProvider, + ReactNodeList, +} from 'shared/ReactTypes'; + +export function createContext(defaultValue: T): ReactContext { + const context = { + provide(value: T, children: ReactNodeList, key?: string): ReactProvider { + return { + $$typeof: REACT_PROVIDER_TYPE, + key: key === null || key === undefined ? null : '' + key, + context, // Recursive + value, + children, + }; + }, + consume( + render: (value: T) => ReactNodeList, + key?: string, + ): ReactConsumer { + return { + $$typeof: REACT_CONSUMER_TYPE, + key: key === null || key === undefined ? null : '' + key, + context, // Recursive + memoizedValue: null, + render, + }; + }, + defaultValue, + lastProvider: null, + }; + return context; +} diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index de205bd2d244e..3398d987ca718 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -27,6 +27,12 @@ export const REACT_FRAGMENT_TYPE = hasSymbol export const REACT_STRICT_MODE_TYPE = hasSymbol ? Symbol.for('react.strict_mode') : 0xeacc; +export const REACT_PROVIDER_TYPE = hasSymbol + ? Symbol.for('react.provider') + : 0xeacd; +export const REACT_CONSUMER_TYPE = hasSymbol + ? Symbol.for('react.consumer') + : 0xeace; const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; const FAUX_ITERATOR_SYMBOL = '@@iterator'; diff --git a/packages/shared/ReactTypeOfWork.js b/packages/shared/ReactTypeOfWork.js index 899323c6c4c77..59add8379a9ae 100644 --- a/packages/shared/ReactTypeOfWork.js +++ b/packages/shared/ReactTypeOfWork.js @@ -7,7 +7,21 @@ * @flow */ -export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; +export type TypeOfWork = + | 0 + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13; export const IndeterminateComponent = 0; // Before we know whether it is functional or class export const FunctionalComponent = 1; @@ -21,3 +35,5 @@ export const CallHandlerPhase = 8; export const ReturnComponent = 9; export const Fragment = 10; export const Mode = 11; +export const ProviderComponent = 12; +export const ConsumerComponent = 13; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index c68864323d7cb..65e936821b527 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -14,7 +14,9 @@ export type ReactNode = | ReactReturn | ReactPortal | ReactText - | ReactFragment; + | ReactFragment + | ReactProvider + | ReactConsumer; export type ReactFragment = ReactEmpty | Iterable; @@ -55,3 +57,27 @@ export type ReactPortal = { // TODO: figure out the API for cross-renderer implementation. implementation: any, }; + +export type ReactProvider = { + $$typeof: Symbol | number, + key: null | string, + context: ReactContext, + value: T, + children: ReactNodeList, +}; + +export type ReactConsumer = { + $$typeof: Symbol | number, + key: null | string, + context: ReactContext, + memoizedValue: T | null, + // TODO: ReactCall calls this "handler." Which one should we use? + render: (value: T) => ReactNodeList, +}; + +export type ReactContext = { + provide(value: T, children: ReactNodeList, key?: string): ReactProvider, + consume(render: (value: T) => ReactNodeList, key?: string): ReactConsumer, + defaultValue: T, + lastProvider: any, // Fiber | null +}; From 191ee7051725cd4fcf6830b9ed7fb6870b36463d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 11 Dec 2017 16:21:59 -0800 Subject: [PATCH 02/17] Fuzz tester for context --- package.json | 1 + .../src/__tests__/ReactNewContext-test.js | 211 +++++++++++++++++- yarn.lock | 6 + 3 files changed, 217 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cf5e65904692..e12b9ccd97e8b 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "platform": "^1.1.0", "prettier": "1.8.1", "prop-types": "^15.6.0", + "random-seed": "^0.3.0", "rimraf": "^2.6.1", "rollup": "^0.52.1", "rollup-plugin-babel": "^3.0.1", diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js index 607b8385f7c7b..a01631448a25a 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js @@ -9,14 +9,16 @@ 'use strict'; -let React; +let React = require('react'); let ReactNoop; +let gen; describe('ReactNewContext', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactNoop = require('react-noop-renderer'); + gen = require('random-seed'); }); // function div(...children) { @@ -303,4 +305,211 @@ describe('ReactNewContext', () => { span('Result: 4'), ]); }); + + describe('fuzz test', () => { + const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; + const contexts = new Map( + contextKeys.map(key => { + const Context = React.createContext(0); + Context.displayName = 'Context' + key; + return [key, Context]; + }), + ); + const Fragment = React.Fragment; + + 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; + } + + 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 Context.consume( + value => ( + + + + + ), + i, + ); + }); + return consumers; + } + } + + function Root(props) { + return contextKeys.reduceRight((children, key) => { + const Context = contexts.get(key); + const value = props.values[key]; + return Context.provide(value, 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 ContextSimulator(maxDepth) { + 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/yarn.lock b/yarn.lock index 72cc17ce0f3f1..04dc3dee1ca45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4373,6 +4373,12 @@ qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" +random-seed@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/random-seed/-/random-seed-0.3.0.tgz#d945f2e1f38f49e8d58913431b8bf6bb937556cd" + dependencies: + json-stringify-safe "^5.0.1" + randomatic@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" From 1dabc74256d61cd4e11c620bef5a0eee4fa40925 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 12 Dec 2017 20:42:08 -0800 Subject: [PATCH 03/17] Use ReactElement for provider and consumer children --- .../react-reconciler/src/ReactChildFiber.js | 214 +----------------- packages/react-reconciler/src/ReactFiber.js | 150 ++++++------ .../src/ReactFiberBeginWork.js | 64 +++--- .../src/ReactFiberCompleteWork.js | 4 +- packages/shared/ReactContext.js | 41 +++- packages/shared/ReactTypes.js | 42 +++- 6 files changed, 171 insertions(+), 344 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 220269fc499d8..1722bf81263df 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -8,11 +8,7 @@ */ import type {ReactElement} from 'shared/ReactElementType'; -import type { - ReactPortal, - ReactProvider, - ReactConsumer, -} from 'shared/ReactTypes'; +import type {ReactPortal} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime'; @@ -22,8 +18,6 @@ import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, - REACT_PROVIDER_TYPE, - REACT_CONSUMER_TYPE, } from 'shared/ReactSymbols'; import { FunctionalComponent, @@ -31,8 +25,6 @@ import { HostText, HostPortal, Fragment, - ProviderComponent, - ConsumerComponent, } from 'shared/ReactTypeOfWork'; import emptyObject from 'fbjs/lib/emptyObject'; import invariant from 'fbjs/lib/invariant'; @@ -44,8 +36,6 @@ import { createFiberFromFragment, createFiberFromText, createFiberFromPortal, - createFiberFromProvider, - createFiberFromConsumer, } from './ReactFiber'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; @@ -423,52 +413,6 @@ function ChildReconciler(shouldTrackSideEffects) { } } - function updateProviderComponent( - returnFiber: Fiber, - current: Fiber | null, - provider: ReactProvider, - expirationTime: ExpirationTime, - ) { - if (current !== null && current.type === provider.context) { - // Move based on index - const existing = useFiber(current, provider, expirationTime); - existing.return = returnFiber; - return existing; - } else { - // Insert - const created = createFiberFromProvider( - provider, - returnFiber.internalContextTag, - expirationTime, - ); - created.return = returnFiber; - return created; - } - } - - function updateConsumerComponent( - returnFiber: Fiber, - current: Fiber | null, - consumer: ReactConsumer, - expirationTime: ExpirationTime, - ) { - if (current !== null && current.type === consumer.context) { - // Move based on index - const existing = useFiber(current, consumer, expirationTime); - existing.return = returnFiber; - return existing; - } else { - // Insert - const created = createFiberFromConsumer( - consumer, - returnFiber.internalContextTag, - expirationTime, - ); - created.return = returnFiber; - return created; - } - } - function createChild( returnFiber: Fiber, newChild: any, @@ -508,24 +452,6 @@ function ChildReconciler(shouldTrackSideEffects) { created.return = returnFiber; return created; } - case REACT_PROVIDER_TYPE: { - const created = createFiberFromProvider( - newChild, - returnFiber.internalContextTag, - expirationTime, - ); - created.return = returnFiber; - return created; - } - case REACT_CONSUMER_TYPE: { - const created = createFiberFromConsumer( - newChild, - returnFiber.internalContextTag, - expirationTime, - ); - created.return = returnFiber; - return created; - } } if (isArray(newChild) || getIteratorFn(newChild)) { @@ -611,30 +537,6 @@ function ChildReconciler(shouldTrackSideEffects) { return null; } } - case REACT_PROVIDER_TYPE: { - if (newChild.key === key) { - return updateProviderComponent( - returnFiber, - oldFiber, - newChild, - expirationTime, - ); - } else { - return null; - } - } - case REACT_CONSUMER_TYPE: { - if (newChild.key === key) { - return updateConsumerComponent( - returnFiber, - oldFiber, - newChild, - expirationTime, - ); - } else { - return null; - } - } } if (isArray(newChild) || getIteratorFn(newChild)) { @@ -717,30 +619,6 @@ function ChildReconciler(shouldTrackSideEffects) { expirationTime, ); } - case REACT_PROVIDER_TYPE: { - const matchedFiber = - existingChildren.get( - newChild.key === null ? newIdx : newChild.key, - ) || null; - return updateProviderComponent( - returnFiber, - matchedFiber, - newChild, - expirationTime, - ); - } - case REACT_CONSUMER_TYPE: { - const matchedFiber = - existingChildren.get( - newChild.key === null ? newIdx : newChild.key, - ) || null; - return updateConsumerComponent( - returnFiber, - matchedFiber, - newChild, - expirationTime, - ); - } } if (isArray(newChild) || getIteratorFn(newChild)) { @@ -1281,78 +1159,6 @@ function ChildReconciler(shouldTrackSideEffects) { return created; } - function reconcileSingleProvider( - returnFiber: Fiber, - currentFirstChild: Fiber | null, - provider: ReactProvider, - expirationTime: ExpirationTime, - ): Fiber { - const key = provider.key; - let child = currentFirstChild; - while (child !== null) { - // TODO: If key === null and child.key === null, then this only applies to - // the first item in the list. - if (child.key === key && child.type === provider.context) { - if (child.tag === ProviderComponent) { - deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, provider, expirationTime); - existing.return = returnFiber; - return existing; - } else { - deleteRemainingChildren(returnFiber, child); - break; - } - } else { - deleteChild(returnFiber, child); - } - child = child.sibling; - } - - const created = createFiberFromProvider( - provider, - returnFiber.internalContextTag, - expirationTime, - ); - created.return = returnFiber; - return created; - } - - function reconcileSingleConsumer( - returnFiber: Fiber, - currentFirstChild: Fiber | null, - consumer: ReactConsumer, - expirationTime: ExpirationTime, - ): Fiber { - const key = consumer.key; - let child = currentFirstChild; - while (child !== null) { - // TODO: If key === null and child.key === null, then this only applies to - // the first item in the list. - if (child.key === key && child.type === consumer.context) { - if (child.tag === ConsumerComponent) { - deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, consumer, expirationTime); - existing.return = returnFiber; - return existing; - } else { - deleteRemainingChildren(returnFiber, child); - break; - } - } else { - deleteChild(returnFiber, child); - } - child = child.sibling; - } - - const created = createFiberFromConsumer( - consumer, - returnFiber.internalContextTag, - expirationTime, - ); - created.return = returnFiber; - return created; - } - // This API will tag the children with the side-effect of the reconciliation // itself. They will be added to the side-effect list as we pass through the // children and the parent. @@ -1402,24 +1208,6 @@ function ChildReconciler(shouldTrackSideEffects) { expirationTime, ), ); - case REACT_PROVIDER_TYPE: - return placeSingleChild( - reconcileSingleProvider( - returnFiber, - currentFirstChild, - newChild, - expirationTime, - ), - ); - case REACT_CONSUMER_TYPE: - return placeSingleChild( - reconcileSingleConsumer( - returnFiber, - currentFirstChild, - newChild, - expirationTime, - ), - ); } } diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 356f5e230a592..821f7dd6acc18 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -7,11 +7,7 @@ */ import type {ReactElement, Source} from 'shared/ReactElementType'; -import type { - ReactPortal, - ReactProvider, - ReactConsumer, -} from 'shared/ReactTypes'; +import type {ReactPortal} from 'shared/ReactTypes'; import type {TypeOfWork} from 'shared/ReactTypeOfWork'; import type {TypeOfInternalContext} from './ReactTypeOfInternalContext'; import type {TypeOfSideEffect} from 'shared/ReactTypeOfSideEffect'; @@ -47,6 +43,8 @@ import { REACT_RETURN_TYPE, REACT_CALL_TYPE, REACT_STRICT_MODE_TYPE, + REACT_PROVIDER_TYPE, + REACT_CONSUMER_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -373,48 +371,45 @@ export function createFiberFromElement( fiber.type = REACT_RETURN_TYPE; break; default: { - if ( - typeof type === 'object' && - type !== null && - typeof type.tag === 'number' - ) { - // Currently assumed to be a continuation and therefore is a - // fiber already. - // TODO: The yield system is currently broken for updates in some - // cases. The reified yield stores a fiber, but we don't know which - // fiber that is; the current or a workInProgress? When the - // continuation gets rendered here we don't know if we can reuse that - // fiber or if we need to clone it. There is probably a clever way to - // restructure this. - fiber = ((type: any): Fiber); - fiber.pendingProps = pendingProps; - } else { - let info = ''; - if (__DEV__) { - if ( - type === undefined || - (typeof type === 'object' && - type !== null && - Object.keys(type).length === 0) - ) { - info += - ' You likely forgot to export your component from the file ' + - "it's defined in, or you might have mixed up default and " + - 'named imports.'; - } - const ownerName = owner ? getComponentName(owner) : null; - if (ownerName) { - info += '\n\nCheck the render method of `' + ownerName + '`.'; - } + if (typeof type === 'object' && type !== null) { + switch (type.$$typeof) { + case REACT_PROVIDER_TYPE: + fiber = createFiber( + ProviderComponent, + pendingProps, + key, + internalContextTag, + ); + fiber.type = type; + break; + case REACT_CONSUMER_TYPE: + fiber = createFiber( + ConsumerComponent, + pendingProps, + key, + internalContextTag, + ); + fiber.type = type; + break; + default: + if (typeof type.tag === 'number') { + // Currently assumed to be a continuation and therefore is a + // fiber already. + // TODO: The yield system is currently broken for updates in some + // cases. The reified yield stores a fiber, but we don't know which + // fiber that is; the current or a workInProgress? When the + // continuation gets rendered here we don't know if we can reuse + // that fiber or if we need to clone it. There is probably a clever + // way to restructure this. + fiber = ((type: any): Fiber); + fiber.pendingProps = pendingProps; + } else { + throwOnInvalidElementType(type, owner); + } + break; } - invariant( - false, - 'Element type is invalid: expected a string (for built-in ' + - 'components) or a class/function (for composite components) ' + - 'but got: %s.%s', - type == null ? type : typeof type, - info, - ); + } else { + throwOnInvalidElementType(type, owner); } } } @@ -430,6 +425,35 @@ export function createFiberFromElement( return fiber; } +function throwOnInvalidElementType(type, owner) { + let info = ''; + if (__DEV__) { + if ( + type === undefined || + (typeof type === 'object' && + type !== null && + Object.keys(type).length === 0) + ) { + info += + ' You likely forgot to export your component from the file ' + + "it's defined in, or you might have mixed up default and " + + 'named imports.'; + } + const ownerName = owner ? getComponentName(owner) : null; + if (ownerName) { + info += '\n\nCheck the render method of `' + ownerName + '`.'; + } + } + invariant( + false, + 'Element type is invalid: expected a string (for built-in ' + + 'components) or a class/function (for composite components) ' + + 'but got: %s.%s', + type == null ? type : typeof type, + info, + ); +} + export function createFiberFromFragment( elements: ReactFragment, internalContextTag: TypeOfInternalContext, @@ -477,37 +501,3 @@ export function createFiberFromPortal( }; return fiber; } - -export function createFiberFromProvider( - provider: ReactProvider, - internalContextTag: TypeOfInternalContext, - expirationTime: ExpirationTime, -): Fiber { - const pendingProps = provider; - const fiber = createFiber( - ProviderComponent, - pendingProps, - provider.key, - internalContextTag, - ); - fiber.expirationTime = expirationTime; - fiber.type = provider.context; - return fiber; -} - -export function createFiberFromConsumer( - consumer: ReactConsumer, - internalContextTag: TypeOfInternalContext, - expirationTime: ExpirationTime, -): Fiber { - const pendingProps = consumer; - const fiber = createFiber( - ConsumerComponent, - pendingProps, - consumer.key, - internalContextTag, - ); - fiber.expirationTime = expirationTime; - fiber.type = consumer.context; - return fiber; -} diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 5edc544026605..74bc9bb39b300 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -9,8 +9,8 @@ import type {HostConfig} from 'react-reconciler'; import type { - ReactProvider, - ReactConsumer, + ReactProviderType, + ReactConsumerType, ReactContext, } from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; @@ -663,7 +663,7 @@ export default function( } function pushContextProvider(workInProgress) { - const context: ReactContext = workInProgress.type; + const context: ReactContext = workInProgress.type.context; // Store a reference to the previous provider // TODO: Only need to do this on mount workInProgress.stateNode = context.lastProvider; @@ -683,7 +683,7 @@ export default function( switch (fiber.tag) { case ConsumerComponent: // Check if the context matches. - if (fiber.type === context) { + if (fiber.type.context === context) { // Update the expiration time of all the ancestors, including // the alternates. let node = fiber; @@ -762,32 +762,33 @@ export default function( workInProgress, renderExpirationTime, ) { - const context: ReactContext = workInProgress.type; + const providerType: ReactProviderType = workInProgress.type; + const context: ReactContext = providerType.context; - const newProvider: ReactProvider = workInProgress.pendingProps; - const oldProvider: ReactProvider | null = workInProgress.memoizedProps; + const newProps = workInProgress.pendingProps; + const oldProps = workInProgress.memoizedProps; pushContextProvider(workInProgress); 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 (oldProvider === newProvider) { + } else if (oldProps === newProps) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } - workInProgress.memoizedProps = newProvider; + workInProgress.memoizedProps = newProps; - const newValue = newProvider.value; - const oldValue = oldProvider !== null ? oldProvider.value : null; + const newValue = newProps.value; + const oldValue = oldProps !== null ? oldProps.value : null; // TODO: Use Object.is instead of === if (newValue !== oldValue) { propagateContextChange(workInProgress, context, renderExpirationTime); } - if (oldProvider !== null && oldProvider.children === newProvider.children) { + if (oldProps !== null && oldProps.children === newProps.children) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } - const newChildren = newProvider.children; + const newChildren = newProps.children; reconcileChildren(current, workInProgress, newChildren); return workInProgress.child; } @@ -797,10 +798,11 @@ export default function( workInProgress, renderExpirationTime, ) { - const context: ReactContext = workInProgress.type; + const consumerType: ReactConsumerType = workInProgress.type; + const context: ReactContext = consumerType.context; - const newConsumer: ReactConsumer = workInProgress.pendingProps; - const oldConsumer: ReactConsumer = workInProgress.memoizedProps; + const newProps = workInProgress.pendingProps; + const oldProps = workInProgress.memoizedProps; // Get the nearest ancestor provider. const providerFiber: Fiber | null = context.lastProvider; @@ -818,28 +820,32 @@ export default function( 'a bug in React. Please file an issue.', ); newValue = provider.value; + + // Context change propagation stops at matching consumers, for time- + // slicing. Continue the propagation here. + if (oldProps === null) { + propagateContextChange(workInProgress, context, renderExpirationTime); + } else { + const oldValue = oldProps !== null ? oldProps.__memoizedValue : null; + // TODO: Use Object.is instead of === + if (newValue !== oldValue) { + propagateContextChange(workInProgress, context, renderExpirationTime); + } + } } + // The old context value is stored on the consumer object. We can't use the // provider's memoizedProps because those have already been updated by the // time we get here, in the provider's begin phase. - const oldValue = oldConsumer !== null ? oldConsumer.memoizedValue : null; - newConsumer.memoizedValue = newValue; + newProps.__memoizedValue = newValue; - // Context change propagation stops at matching consumers, for time-slicing. - // Continue the propagation here. - // TODO: Use Object.is instead of === - if (newValue !== oldValue) { - propagateContextChange(workInProgress, context, renderExpirationTime); - // Because the context value has changed, do not bail out, even if the - // consumer objects match. - } else if (hasLegacyContextChanged()) { + 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 (newConsumer === oldConsumer) { + } else if (newProps === oldProps) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } - - const newChildren = newConsumer.render(newValue); + const newChildren = newProps.render(newValue); reconcileChildren(current, workInProgress, newChildren); return workInProgress.child; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index f3302d20dd68e..6414ef72f708f 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -7,6 +7,7 @@ * @flow */ +import type {ReactProviderType, ReactContext} from 'shared/ReactTypes'; import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; @@ -586,7 +587,8 @@ export default function( updateHostContainer(workInProgress); return null; case ProviderComponent: - const context = workInProgress.type; + const providerType: ReactProviderType = workInProgress.type; + const context: ReactContext = providerType.context; // Pop provider fiber const lastProvider = workInProgress.stateNode; context.lastProvider = lastProvider; diff --git a/packages/shared/ReactContext.js b/packages/shared/ReactContext.js index 63a41d2857442..0aae0810a16cf 100644 --- a/packages/shared/ReactContext.js +++ b/packages/shared/ReactContext.js @@ -7,7 +7,11 @@ * @flow */ -import {REACT_PROVIDER_TYPE, REACT_CONSUMER_TYPE} from 'shared/ReactSymbols'; +import { + REACT_PROVIDER_TYPE, + REACT_CONSUMER_TYPE, + REACT_ELEMENT_TYPE, +} from 'shared/ReactSymbols'; import type { ReactContext, @@ -17,14 +21,20 @@ import type { } from 'shared/ReactTypes'; export function createContext(defaultValue: T): ReactContext { + let providerType; + let consumerType; + const context = { provide(value: T, children: ReactNodeList, key?: string): ReactProvider { return { - $$typeof: REACT_PROVIDER_TYPE, + $$typeof: REACT_ELEMENT_TYPE, + type: providerType, key: key === null || key === undefined ? null : '' + key, - context, // Recursive - value, - children, + ref: null, + props: { + value, + children, + }, }; }, consume( @@ -32,15 +42,28 @@ export function createContext(defaultValue: T): ReactContext { key?: string, ): ReactConsumer { return { - $$typeof: REACT_CONSUMER_TYPE, + $$typeof: REACT_ELEMENT_TYPE, + type: consumerType, key: key === null || key === undefined ? null : '' + key, - context, // Recursive - memoizedValue: null, - render, + ref: null, + props: { + render, + __memoizedValue: null, + }, }; }, defaultValue, lastProvider: null, }; + + providerType = { + $$typeof: REACT_PROVIDER_TYPE, + context, + }; + consumerType = { + $$typeof: REACT_CONSUMER_TYPE, + context, + }; + return context; } diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 65e936821b527..05f9db03f738f 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -1,3 +1,5 @@ +import {Symbol} from './node_modules/typescript/lib/typescript'; + /** * Copyright (c) 2014-present, Facebook, Inc. * @@ -49,30 +51,37 @@ export type ReactReturn = { }, }; -export type ReactPortal = { +export type ReactProvider = { $$typeof: Symbol | number, + type: ReactProviderType, key: null | string, - containerInfo: any, - children: ReactNodeList, - // TODO: figure out the API for cross-renderer implementation. - implementation: any, + ref: null, + props: { + value: T, + children?: ReactNodeList, + }, }; -export type ReactProvider = { +export type ReactProviderType = { $$typeof: Symbol | number, - key: null | string, context: ReactContext, - value: T, - children: ReactNodeList, }; export type ReactConsumer = { $$typeof: Symbol | number, + type: ReactConsumerType, key: null | string, + ref: null, + props: { + render: (value: T) => ReactNodeList, + // TODO: Remove this hack + __memoizedValue: T | null, + }, +}; + +export type ReactConsumerType = { + $$typeof: Symbol | number, context: ReactContext, - memoizedValue: T | null, - // TODO: ReactCall calls this "handler." Which one should we use? - render: (value: T) => ReactNodeList, }; export type ReactContext = { @@ -81,3 +90,12 @@ export type ReactContext = { defaultValue: T, lastProvider: any, // Fiber | null }; + +export type ReactPortal = { + $$typeof: Symbol | number, + key: null | string, + containerInfo: any, + children: ReactNodeList, + // TODO: figure out the API for cross-renderer implementation. + implementation: any, +}; From 2c9a9062314a0bab319704032c90a8914779653d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 13 Dec 2017 13:30:42 -0800 Subject: [PATCH 04/17] Compare context values using Object.is Same semantics as PureComponent/shallowEqual. --- .../src/ReactFiberBeginWork.js | 16 +++-- .../src/__tests__/ReactNewContext-test.js | 61 +++++++++++++++++++ packages/shared/is.js | 31 ++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 packages/shared/is.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 74bc9bb39b300..fa3174fbe0257 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -42,6 +42,7 @@ import { Err, Ref, } from 'shared/ReactTypeOfSideEffect'; +import is from 'shared/is'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import {debugRenderPhaseSideEffects} from 'shared/ReactFeatureFlags'; import invariant from 'fbjs/lib/invariant'; @@ -780,8 +781,9 @@ export default function( const newValue = newProps.value; const oldValue = oldProps !== null ? oldProps.value : null; - // TODO: Use Object.is instead of === - if (newValue !== oldValue) { + + // Use Object.is to compare the new context value to the old value. + if (!is(newValue, oldValue)) { propagateContextChange(workInProgress, context, renderExpirationTime); } @@ -808,10 +810,12 @@ export default function( const providerFiber: Fiber | null = context.lastProvider; let newValue; + let valueDidChange; if (providerFiber === null) { // This is a detached consumer (has no provider). Use the default // context value. newValue = context.defaultValue; + valueDidChange = false; } else { const provider = providerFiber.pendingProps; invariant( @@ -824,11 +828,13 @@ export default function( // Context change propagation stops at matching consumers, for time- // slicing. Continue the propagation here. if (oldProps === null) { + valueDidChange = true; propagateContextChange(workInProgress, context, renderExpirationTime); } else { const oldValue = oldProps !== null ? oldProps.__memoizedValue : null; - // TODO: Use Object.is instead of === - if (newValue !== oldValue) { + // Use Object.is to compare the new context value to the old value. + if (!is(newValue, oldValue)) { + valueDidChange = true; propagateContextChange(workInProgress, context, renderExpirationTime); } } @@ -842,7 +848,7 @@ export default function( 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 (newProps === oldProps) { + } else if (newProps === oldProps && !valueDidChange) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } const newChildren = newProps.render(newValue); diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js index a01631448a25a..b1fb715af14d5 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js @@ -306,6 +306,67 @@ describe('ReactNewContext', () => { ]); }); + it('compares context values with Object.is semantics', () => { + const Context = React.createContext(1); + + function Provider(props) { + ReactNoop.yield('Provider'); + return Context.provide(props.value, props.children); + } + + function Consumer(props) { + ReactNoop.yield('Consumer'); + return Context.consume(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')]); + }); + describe('fuzz test', () => { const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; const contexts = new Map( diff --git a/packages/shared/is.js b/packages/shared/is.js new file mode 100644 index 0000000000000..19a0c49cb0006 --- /dev/null +++ b/packages/shared/is.js @@ -0,0 +1,31 @@ +/** + * 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 + */ + +let is: (x: mixed, y: mixed) => boolean; + +if (typeof Object.is === 'function') { + is = Object.is; +} else { + // inlined Object.is polyfill to avoid requiring consumers ship their own + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + is = (x: mixed, y: mixed) => { + // SameValue algorithm + if (x === y) { + // Steps 1-5, 7-10 + // Steps 6.b-6.e: +0 != -0 + // Added the nonzero y check to make Flow happy, but it is redundant + return x !== 0 || y !== 0 || 1 / x === 1 / y; + } else { + // Step 6.a: NaN == NaN + return x !== x && y !== y; // eslint-disable-line no-self-compare + } + }; +} + +export default is; From e3d66b3a6dd97ab2ecf6220fd907daef49b34f77 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 12 Dec 2017 20:58:17 -0800 Subject: [PATCH 05/17] Unify more branches in createFiberFromElement --- packages/react-reconciler/src/ReactFiber.js | 78 ++++++--------------- 1 file changed, 23 insertions(+), 55 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 821f7dd6acc18..5690b52679e69 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -320,20 +320,13 @@ export function createFiberFromElement( let fiber; const type = element.type; const key = element.key; - const pendingProps = element.props; + let pendingProps = element.props; + + let fiberTag; if (typeof type === 'function') { - fiber = shouldConstruct(type) - ? createFiber(ClassComponent, pendingProps, key, internalContextTag) - : createFiber( - IndeterminateComponent, - pendingProps, - key, - internalContextTag, - ); - fiber.type = type; + fiberTag = shouldConstruct(type) ? ClassComponent : IndeterminateComponent; } else if (typeof type === 'string') { - fiber = createFiber(HostComponent, pendingProps, key, internalContextTag); - fiber.type = type; + fiberTag = HostComponent; } else { switch (type) { case REACT_FRAGMENT_TYPE: @@ -344,65 +337,38 @@ export function createFiberFromElement( key, ); case REACT_STRICT_MODE_TYPE: - fiber = createFiber( - Mode, - pendingProps, - key, - internalContextTag | StrictMode, - ); - fiber.type = REACT_STRICT_MODE_TYPE; + fiberTag = Mode; + internalContextTag |= StrictMode; break; case REACT_CALL_TYPE: - fiber = createFiber( - CallComponent, - pendingProps, - key, - internalContextTag, - ); - fiber.type = REACT_CALL_TYPE; + fiberTag = CallComponent; break; case REACT_RETURN_TYPE: - fiber = createFiber( - ReturnComponent, - pendingProps, - key, - internalContextTag, - ); - fiber.type = REACT_RETURN_TYPE; + fiberTag = ReturnComponent; break; default: { if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { case REACT_PROVIDER_TYPE: - fiber = createFiber( - ProviderComponent, - pendingProps, - key, - internalContextTag, - ); - fiber.type = type; + fiberTag = ProviderComponent; break; case REACT_CONSUMER_TYPE: - fiber = createFiber( - ConsumerComponent, - pendingProps, - key, - internalContextTag, - ); - fiber.type = type; + fiberTag = ConsumerComponent; break; default: if (typeof type.tag === 'number') { // Currently assumed to be a continuation and therefore is a // fiber already. - // TODO: The yield system is currently broken for updates in some - // cases. The reified yield stores a fiber, but we don't know which - // fiber that is; the current or a workInProgress? When the - // continuation gets rendered here we don't know if we can reuse - // that fiber or if we need to clone it. There is probably a clever - // way to restructure this. + // TODO: The yield system is currently broken for updates in + // some cases. The reified yield stores a fiber, but we don't + // know which fiber that is; the current or a workInProgress? + // When the continuation gets rendered here we don't know if we + // can reuse that fiber or if we need to clone it. There is + // probably a clever way to restructure this. fiber = ((type: any): Fiber); fiber.pendingProps = pendingProps; + fiber.expirationTime = expirationTime; + return fiber; } else { throwOnInvalidElementType(type, owner); } @@ -415,13 +381,15 @@ export function createFiberFromElement( } } + fiber = createFiber(fiberTag, pendingProps, key, internalContextTag); + fiber.type = type; + fiber.expirationTime = expirationTime; + if (__DEV__) { fiber._debugSource = element._source; fiber._debugOwner = element._owner; } - fiber.expirationTime = expirationTime; - return fiber; } From 4baa415d2968f61ba23f94e6733d3fa52aab2c5e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 13 Dec 2017 16:12:45 -0800 Subject: [PATCH 06/17] Store providers on global stack Rather than using a linked list stored on the context type. The global stack can be reset in case of an interruption or error, whereas with the linked list implementation, you'd need to keep track of every context type. --- .../src/ReactFiberBeginWork.js | 16 ++---- .../src/ReactFiberCompleteWork.js | 15 +++--- .../src/ReactFiberNewContext.js | 46 ++++++++++++++++ .../src/ReactFiberScheduler.js | 11 +++- .../src/__tests__/ReactNewContext-test.js | 53 +++++++++++++++++++ packages/shared/ReactContext.js | 1 - packages/shared/ReactTypes.js | 1 - 7 files changed, 118 insertions(+), 25 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberNewContext.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index fa3174fbe0257..97ace86175707 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -66,6 +66,7 @@ import { pushTopLevelContextObject, invalidateContextProvider, } from './ReactFiberContext'; +import {pushProvider, getProvider} from './ReactFiberNewContext'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncUpdates} from './ReactTypeOfInternalContext'; @@ -663,15 +664,6 @@ export default function( return workInProgress.child; } - function pushContextProvider(workInProgress) { - const context: ReactContext = workInProgress.type.context; - // Store a reference to the previous provider - // TODO: Only need to do this on mount - workInProgress.stateNode = context.lastProvider; - // Push this onto the list of providers. We'll pop in the complete phase. - context.lastProvider = workInProgress; - } - function propagateContextChange( workInProgress: Fiber, context: ReactContext, @@ -769,7 +761,7 @@ export default function( const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - pushContextProvider(workInProgress); + pushProvider(workInProgress); if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed @@ -807,7 +799,7 @@ export default function( const oldProps = workInProgress.memoizedProps; // Get the nearest ancestor provider. - const providerFiber: Fiber | null = context.lastProvider; + const providerFiber: Fiber | null = getProvider(context); let newValue; let valueDidChange; @@ -918,7 +910,7 @@ export default function( ); break; case ProviderComponent: - pushContextProvider(workInProgress); + pushProvider(workInProgress); break; } // TODO: What if this is currently in progress? diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 6414ef72f708f..53db89213da6f 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -7,7 +7,6 @@ * @flow */ -import type {ReactProviderType, ReactContext} from 'shared/ReactTypes'; import type {HostConfig} from 'react-reconciler'; import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; @@ -41,9 +40,10 @@ import invariant from 'fbjs/lib/invariant'; import {reconcileChildFibers} from './ReactChildFiber'; import { - popContextProvider, - popTopLevelContextObject, + popContextProvider as popLegacyContextProvider, + popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext'; +import {popProvider} from './ReactFiberNewContext'; export default function( config: HostConfig, @@ -404,12 +404,12 @@ export default function( return null; case ClassComponent: { // We are leaving this subtree, so pop context if any. - popContextProvider(workInProgress); + popLegacyContextProvider(workInProgress); return null; } case HostRoot: { popHostContainer(workInProgress); - popTopLevelContextObject(workInProgress); + popTopLevelLegacyContextObject(workInProgress); const fiberRoot = (workInProgress.stateNode: FiberRoot); if (fiberRoot.pendingContext) { fiberRoot.context = fiberRoot.pendingContext; @@ -587,11 +587,8 @@ export default function( updateHostContainer(workInProgress); return null; case ProviderComponent: - const providerType: ReactProviderType = workInProgress.type; - const context: ReactContext = providerType.context; // Pop provider fiber - const lastProvider = workInProgress.stateNode; - context.lastProvider = lastProvider; + popProvider(workInProgress); return null; case ConsumerComponent: return null; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js new file mode 100644 index 0000000000000..bf6a24943c63e --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -0,0 +1,46 @@ +/** + * 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 warning from 'fbjs/lib/warning'; + +let stack: Array = []; +let index = -1; + +export function pushProvider(providerFiber: Fiber): void { + index += 1; + stack[index] = providerFiber; +} + +export function popProvider(providerFiber: Fiber): void { + if (__DEV__) { + warning(index > -1 && providerFiber === stack[index], 'Unexpected pop.'); + } + stack[index] = null; + index -= 1; +} + +// Find the nearest matching provider +export function getProvider(context: ReactContext): Fiber | null { + for (let i = index; i > -1; i--) { + const provider = stack[i]; + if (provider.type.context === context) { + return provider; + } + } + return null; +} + +export function resetProviderStack(): void { + for (let i = index; i > -1; i--) { + stack[i] = null; + } +} diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index a2d0a5d354d59..2991435c1ec3b 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -33,6 +33,7 @@ import { HostComponent, HostPortal, ClassComponent, + ProviderComponent, } from 'shared/ReactTypeOfWork'; import {enableUserTimingAPI} from 'shared/ReactFeatureFlags'; import getComponentName from 'shared/getComponentName'; @@ -64,6 +65,7 @@ import { stopCommitLifeCyclesTimer, } from './ReactDebugFiberPerf'; import {popContextProvider} from './ReactFiberContext'; +import {popProvider} from './ReactFiberNewContext'; import {reset} from './ReactFiberStack'; import {logCapturedError} from './ReactFiberErrorLogger'; import {createWorkInProgress} from './ReactFiber'; @@ -78,7 +80,8 @@ import { } from './ReactFiberExpirationTime'; import {AsyncUpdates} from './ReactTypeOfInternalContext'; import {getUpdateExpirationTime} from './ReactFiberUpdateQueue'; -import {resetContext} from './ReactFiberContext'; +import {resetContext as resetLegacyContext} from './ReactFiberContext'; +import {resetProviderStack} from './ReactFiberNewContext'; const { invokeGuardedCallback, @@ -238,7 +241,8 @@ export default function( // Reset the stack reset(); // Reset the cursors - resetContext(); + resetLegacyContext(); + resetProviderStack(); resetHostContainer(); } @@ -1134,6 +1138,9 @@ export default function( case HostPortal: popHostContainer(node); break; + case ProviderComponent: + popProvider(node); + break; } if (node === to || node.alternate === to) { stopFailedWorkTimer(node); diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js index b1fb715af14d5..88c2f2c7516bc 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js @@ -367,6 +367,59 @@ describe('ReactNewContext', () => { expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]); }); + it('context unwinds when interrupted', () => { + const Context = React.createContext('Default'); + + function Provider(props) { + return Context.provide(props.value, props.children); + } + + function Consumer(props) { + return Context.consume(value => { + return ; + }); + } + + 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. This proves the + span('Result: Does not unwind'), + ]); + }); + describe('fuzz test', () => { const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; const contexts = new Map( diff --git a/packages/shared/ReactContext.js b/packages/shared/ReactContext.js index 0aae0810a16cf..52cf550985bfe 100644 --- a/packages/shared/ReactContext.js +++ b/packages/shared/ReactContext.js @@ -53,7 +53,6 @@ export function createContext(defaultValue: T): ReactContext { }; }, defaultValue, - lastProvider: null, }; providerType = { diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 05f9db03f738f..8d9faf94be125 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -88,7 +88,6 @@ export type ReactContext = { provide(value: T, children: ReactNodeList, key?: string): ReactProvider, consume(render: (value: T) => ReactNodeList, key?: string): ReactConsumer, defaultValue: T, - lastProvider: any, // Fiber | null }; export type ReactPortal = { From 63d366ce7c3fa464454077ff4c9e649e4e2cb06f Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 13 Dec 2017 15:18:13 -0800 Subject: [PATCH 07/17] Add support for Provider and Consumer to server-side renderer --- .../__tests__/ReactServerRendering-test.js | 40 ++++++ .../src/server/ReactPartialRenderer.js | 126 +++++++++++++++++- 2 files changed, 161 insertions(+), 5 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index f37fa2c15cd6e..b8975af3a471f 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -384,6 +384,46 @@ describe('ReactDOMServer', () => { expect(markup).toContain('hello, world'); }); + it('renders with new context API', () => { + const Context = React.createContext(0); + + function Provider(props) { + return Context.provide(props.value, props.children); + } + + function Consumer(props) { + return Context.consume(value => { + return 'Result: ' + value; + }); + } + + const Indirection = React.Fragment; + + function App(props) { + return ( + + + + + + + + + + + + + + + ); + } + + const markup = ReactDOMServer.renderToString(); + // Extract the numbers rendered by the consumers + const results = markup.match(/\d+/g).map(Number); + expect(results).toEqual([2, 1, 3, 1]); + }); + it('renders components with different batching strategies', () => { class StaticComponent extends React.Component { render() { diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 339411596bc1a..76d761f8311a0 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -8,6 +8,11 @@ */ import type {ReactElement} from 'shared/ReactElementType'; +import type { + ReactProvider, + ReactConsumer, + ReactContext, +} from 'shared/ReactTypes'; import React from 'react'; import emptyFunction from 'fbjs/lib/emptyFunction'; @@ -25,6 +30,8 @@ import { REACT_CALL_TYPE, REACT_RETURN_TYPE, REACT_PORTAL_TYPE, + REACT_PROVIDER_TYPE, + REACT_CONSUMER_TYPE, } from 'shared/ReactSymbols'; import { @@ -117,6 +124,36 @@ if (__DEV__) { }; } +// Context (new API) +let providerStack: Array> = []; // Stack of provider objects +let index = -1; + +export function pushProvider(provider: ReactProvider): void { + index += 1; + providerStack[index] = provider; +} + +export function popProvider(provider: ReactProvider): void { + if (__DEV__) { + warning(index > -1 && provider === providerStack[index], 'Unexpected pop.'); + } + providerStack[index] = null; + index -= 1; +} + +// Find the nearest matching provider +export function getProvider( + context: ReactContext, +): ReactProvider | null { + for (let i = index; i > -1; i--) { + const provider = providerStack[i]; + if (provider.type.context === context) { + return provider; + } + } + return null; +} + let didWarnDefaultInputValue = false; let didWarnDefaultChecked = false; let didWarnDefaultSelectValue = false; @@ -192,7 +229,7 @@ function warnNoop( const constructor = publicInstance.constructor; const componentName = (constructor && getComponentName(constructor)) || 'ReactClass'; - const warningKey = `${componentName}.${callerName}`; + const warningKey = componentName + '.' + callerName; if (didWarnAboutNoopUpdateForComponent[warningKey]) { return; } @@ -603,6 +640,7 @@ function resolve( } type Frame = { + type: mixed, domNamespace: string, children: FlatReactChildren, childIndex: number, @@ -628,6 +666,7 @@ class ReactDOMServerRenderer { const topFrame: Frame = { // Assume all trees start in the HTML namespace (not totally true, but // this is what we did historically) + type: null, domNamespace: Namespaces.html, children: flatChildren, childIndex: 0, @@ -663,8 +702,15 @@ class ReactDOMServerRenderer { this.previousWasTextNode = false; } this.stack.pop(); - if (frame.tag === 'select') { + if (frame.type === 'select') { this.currentSelectValue = null; + } else if ( + frame.type != null && + frame.type.type != null && + frame.type.type.$$typeof === REACT_PROVIDER_TYPE + ) { + const provider: ReactProvider = (frame.type: any); + popProvider(provider); } continue; } @@ -723,6 +769,7 @@ class ReactDOMServerRenderer { } const nextChildren = toArray(nextChild); const frame: Frame = { + type: null, domNamespace: parentNamespace, children: nextChildren, childIndex: 0, @@ -738,12 +785,18 @@ class ReactDOMServerRenderer { // Safe because we just checked it's an element. const nextElement = ((nextChild: any): ReactElement); const elementType = nextElement.type; + + if (typeof elementType === 'string') { + return this.renderDOM(nextElement, context, parentNamespace); + } + switch (elementType) { - case REACT_FRAGMENT_TYPE: + case REACT_FRAGMENT_TYPE: { const nextChildren = toArray( ((nextChild: any): ReactElement).props.children, ); const frame: Frame = { + type: null, domNamespace: parentNamespace, children: nextChildren, childIndex: 0, @@ -755,6 +808,7 @@ class ReactDOMServerRenderer { } this.stack.push(frame); return ''; + } case REACT_CALL_TYPE: case REACT_RETURN_TYPE: invariant( @@ -764,8 +818,70 @@ class ReactDOMServerRenderer { ); // eslint-disable-next-line-no-fallthrough default: - return this.renderDOM(nextElement, context, parentNamespace); + break; + } + if (typeof elementType === 'object' && elementType !== null) { + switch (elementType.$$typeof) { + case REACT_PROVIDER_TYPE: { + const provider: ReactProvider = nextChild; + const nextProps = provider.props; + const nextChildren = toArray(nextProps.children); + const frame: Frame = { + type: provider, + domNamespace: parentNamespace, + children: nextChildren, + childIndex: 0, + context: context, + footer: '', + }; + if (__DEV__) { + ((frame: any): FrameDev).debugElementStack = []; + } + + pushProvider(provider); + + this.stack.push(frame); + return ''; + } + case REACT_CONSUMER_TYPE: { + const consumer: ReactConsumer = nextChild; + const nextProps = consumer.props; + + const provider = getProvider(consumer.type.context); + let nextValue; + if (provider === null) { + // Detached consumer + nextValue = consumer.type.context.defaultValue; + } else { + nextValue = provider.props.value; + } + + const nextChildren = toArray(nextProps.render(nextValue)); + const frame: Frame = { + type: nextChild, + domNamespace: parentNamespace, + children: nextChildren, + childIndex: 0, + context: context, + footer: '', + }; + if (__DEV__) { + ((frame: any): FrameDev).debugElementStack = []; + } + this.stack.push(frame); + return ''; + } + default: + break; + } } + invariant( + false, + 'Element type is invalid: expected a string (for built-in ' + + 'components) or a class/function (for composite components) ' + + 'but got: %s.', + elementType == null ? elementType : typeof elementType, + ); } } @@ -1052,7 +1168,7 @@ class ReactDOMServerRenderer { } const frame = { domNamespace: getChildNamespace(parentNamespace, element.type), - tag, + type: tag, children, childIndex: 0, context: context, From fc77b8d2d1de60c21398fddc9150fa6d704204b6 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 13 Dec 2017 18:14:31 -0800 Subject: [PATCH 08/17] Put new context API behind a feature flag We'll enable this in www only for now. --- ... => ReactServerRendering-test.internal.js} | 5 +- .../src/ReactFiberBeginWork.js | 279 +++++++++--------- ...st.js => ReactNewContext-test.internal.js} | 21 +- packages/react/src/React.js | 8 +- packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fabric.js | 1 + .../shared/forks/ReactFeatureFlags.native.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 8 files changed, 174 insertions(+), 144 deletions(-) rename packages/react-dom/src/__tests__/{ReactServerRendering-test.js => ReactServerRendering-test.internal.js} (98%) rename packages/react-reconciler/src/__tests__/{ReactNewContext-test.js => ReactNewContext-test.internal.js} (95%) diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js similarity index 98% rename from packages/react-dom/src/__tests__/ReactServerRendering-test.js rename to packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js index b8975af3a471f..09a150755536a 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js @@ -14,6 +14,7 @@ let React; let ReactCallReturn; let ReactDOMServer; let PropTypes; +let ReactFeatureFlags; function normalizeCodeLocInfo(str) { return str && str.replace(/\(at .+?:\d+\)/g, '(at **)'); @@ -22,6 +23,8 @@ function normalizeCodeLocInfo(str) { describe('ReactDOMServer', () => { beforeEach(() => { jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableNewContextAPI = true; React = require('react'); ReactCallReturn = require('react-call-return'); PropTypes = require('prop-types'); @@ -385,7 +388,7 @@ describe('ReactDOMServer', () => { }); it('renders with new context API', () => { - const Context = React.createContext(0); + const Context = React.unstable_createContext(0); function Provider(props) { return Context.provide(props.value, props.children); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 97ace86175707..e822d00f540c7 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -19,6 +19,7 @@ import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import {enableNewContextAPI} from 'shared/ReactFeatureFlags'; import { IndeterminateComponent, FunctionalComponent, @@ -669,84 +670,86 @@ export default function( context: ReactContext, renderExpirationTime: ExpirationTime, ): void { - let fiber = workInProgress.child; - while (fiber !== null) { - let nextFiber; - // Visit this fiber. - switch (fiber.tag) { - case ConsumerComponent: - // Check if the context matches. - if (fiber.type.context === context) { - // 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 (enableNewContextAPI) { + let fiber = workInProgress.child; + while (fiber !== null) { + let nextFiber; + // Visit this fiber. + switch (fiber.tag) { + case ConsumerComponent: + // Check if the context matches. + if (fiber.type.context === context) { + // 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; } - } 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; } - 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; } - // 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 { + break; + case ProviderComponent: + // Don't scan deeper if this is a matching provider + nextFiber = fiber.type === context ? null : fiber.child; + break; + default: // Traverse down. nextFiber = fiber.child; - } - break; - case ProviderComponent: - // Don't scan deeper if this is a matching provider - nextFiber = fiber.type === context ? 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) { - nextFiber = sibling; 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) { + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; } - // No more siblings. Traverse up. - nextFiber = nextFiber.return; } + fiber = nextFiber; } - fiber = nextFiber; } } @@ -755,36 +758,40 @@ export default function( workInProgress, renderExpirationTime, ) { - const providerType: ReactProviderType = workInProgress.type; - const context: ReactContext = providerType.context; + if (enableNewContextAPI) { + const providerType: ReactProviderType = workInProgress.type; + const context: ReactContext = providerType.context; - const newProps = workInProgress.pendingProps; - const oldProps = workInProgress.memoizedProps; + const newProps = workInProgress.pendingProps; + const oldProps = workInProgress.memoizedProps; - pushProvider(workInProgress); + pushProvider(workInProgress); - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (oldProps === newProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - workInProgress.memoizedProps = newProps; + if (hasLegacyContextChanged()) { + // Normally we can bail out on props equality but if context has changed + // we don't do the bailout and we have to reuse existing props instead. + } else if (oldProps === newProps) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + workInProgress.memoizedProps = newProps; - const newValue = newProps.value; - const oldValue = oldProps !== null ? oldProps.value : null; + const newValue = newProps.value; + const oldValue = oldProps !== null ? oldProps.value : null; - // Use Object.is to compare the new context value to the old value. - if (!is(newValue, oldValue)) { - propagateContextChange(workInProgress, context, renderExpirationTime); - } + // Use Object.is to compare the new context value to the old value. + if (!is(newValue, oldValue)) { + propagateContextChange(workInProgress, context, renderExpirationTime); + } - if (oldProps !== null && oldProps.children === newProps.children) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); + if (oldProps !== null && oldProps.children === newProps.children) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + const newChildren = newProps.children; + reconcileChildren(current, workInProgress, newChildren); + return workInProgress.child; + } else { + return null; } - const newChildren = newProps.children; - reconcileChildren(current, workInProgress, newChildren); - return workInProgress.child; } function updateConsumerComponent( @@ -792,60 +799,68 @@ export default function( workInProgress, renderExpirationTime, ) { - const consumerType: ReactConsumerType = workInProgress.type; - const context: ReactContext = consumerType.context; - - const newProps = workInProgress.pendingProps; - const oldProps = workInProgress.memoizedProps; - - // Get the nearest ancestor provider. - const providerFiber: Fiber | null = getProvider(context); - - let newValue; - let valueDidChange; - if (providerFiber === null) { - // This is a detached consumer (has no provider). Use the default - // context value. - newValue = context.defaultValue; - valueDidChange = false; - } else { - const provider = providerFiber.pendingProps; - invariant( - provider, - 'Provider should have pending props. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - newValue = provider.value; - - // Context change propagation stops at matching consumers, for time- - // slicing. Continue the propagation here. - if (oldProps === null) { - valueDidChange = true; - propagateContextChange(workInProgress, context, renderExpirationTime); + if (enableNewContextAPI) { + const consumerType: ReactConsumerType = workInProgress.type; + const context: ReactContext = consumerType.context; + + const newProps = workInProgress.pendingProps; + const oldProps = workInProgress.memoizedProps; + + // Get the nearest ancestor provider. + const providerFiber: Fiber | null = getProvider(context); + + let newValue; + let valueDidChange; + if (providerFiber === null) { + // This is a detached consumer (has no provider). Use the default + // context value. + newValue = context.defaultValue; + valueDidChange = false; } else { - const oldValue = oldProps !== null ? oldProps.__memoizedValue : null; - // Use Object.is to compare the new context value to the old value. - if (!is(newValue, oldValue)) { + const provider = providerFiber.pendingProps; + invariant( + provider, + 'Provider should have pending props. This error is likely caused by ' + + 'a bug in React. Please file an issue.', + ); + newValue = provider.value; + + // Context change propagation stops at matching consumers, for time- + // slicing. Continue the propagation here. + if (oldProps === null) { valueDidChange = true; propagateContextChange(workInProgress, context, renderExpirationTime); + } else { + const oldValue = oldProps !== null ? oldProps.__memoizedValue : null; + // Use Object.is to compare the new context value to the old value. + if (!is(newValue, oldValue)) { + valueDidChange = true; + propagateContextChange( + workInProgress, + context, + renderExpirationTime, + ); + } } } - } - // The old context value is stored on the consumer object. We can't use the - // provider's memoizedProps because those have already been updated by the - // time we get here, in the provider's begin phase. - newProps.__memoizedValue = newValue; + // The old context value is stored on the consumer object. We can't use the + // provider's memoizedProps because those have already been updated by the + // time we get here, in the provider's begin phase. + newProps.__memoizedValue = newValue; - 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 (newProps === oldProps && !valueDidChange) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); + 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 (newProps === oldProps && !valueDidChange) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + const newChildren = newProps.render(newValue); + reconcileChildren(current, workInProgress, newChildren); + return workInProgress.child; + } else { + return null; } - const newChildren = newProps.render(newValue); - reconcileChildren(current, workInProgress, newChildren); - return workInProgress.child; } /* diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js similarity index 95% rename from packages/react-reconciler/src/__tests__/ReactNewContext-test.js rename to packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 88c2f2c7516bc..233cc12e6823e 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -9,6 +9,9 @@ 'use strict'; +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); +ReactFeatureFlags.enableNewContextAPI = true; + let React = require('react'); let ReactNoop; let gen; @@ -16,6 +19,8 @@ let gen; describe('ReactNewContext', () => { beforeEach(() => { jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableNewContextAPI = true; React = require('react'); ReactNoop = require('react-noop-renderer'); gen = require('random-seed'); @@ -31,7 +36,7 @@ describe('ReactNewContext', () => { } it('simple mount and update', () => { - const Context = React.createContext(1); + const Context = React.unstable_createContext(1); function Provider(props) { return Context.provide(props.value, props.children); @@ -68,7 +73,7 @@ describe('ReactNewContext', () => { }); it('propagates through shouldComponentUpdate false', () => { - const Context = React.createContext(1); + const Context = React.unstable_createContext(1); function Provider(props) { ReactNoop.yield('Provider'); @@ -128,7 +133,7 @@ describe('ReactNewContext', () => { }); it('consumers bail out if context value is the same', () => { - const Context = React.createContext(1); + const Context = React.unstable_createContext(1); function Provider(props) { ReactNoop.yield('Provider'); @@ -188,7 +193,7 @@ describe('ReactNewContext', () => { }); it('nested providers', () => { - const Context = React.createContext(1); + const Context = React.unstable_createContext(1); function Provider(props) { return Context.consume(contextValue => @@ -241,7 +246,7 @@ describe('ReactNewContext', () => { }); it('multiple consumers in different branches', () => { - const Context = React.createContext(1); + const Context = React.unstable_createContext(1); function Provider(props) { return Context.consume(contextValue => @@ -307,7 +312,7 @@ describe('ReactNewContext', () => { }); it('compares context values with Object.is semantics', () => { - const Context = React.createContext(1); + const Context = React.unstable_createContext(1); function Provider(props) { ReactNoop.yield('Provider'); @@ -368,7 +373,7 @@ describe('ReactNewContext', () => { }); it('context unwinds when interrupted', () => { - const Context = React.createContext('Default'); + const Context = React.unstable_createContext('Default'); function Provider(props) { return Context.provide(props.value, props.children); @@ -424,7 +429,7 @@ describe('ReactNewContext', () => { const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; const contexts = new Map( contextKeys.map(key => { - const Context = React.createContext(0); + const Context = React.unstable_createContext(0); Context.displayName = 'Context' + key; return [key, Context]; }), diff --git a/packages/react/src/React.js b/packages/react/src/React.js index c987a5f16e4f9..110dd0a9c8264 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -25,6 +25,7 @@ import { cloneElementWithValidation, } from './ReactElementValidator'; import ReactDebugCurrentFrame from './ReactDebugCurrentFrame'; +import {enableNewContextAPI} from 'shared/ReactFeatureFlags'; const React = { Children: { @@ -42,9 +43,6 @@ const React = { Fragment: REACT_FRAGMENT_TYPE, StrictMode: REACT_STRICT_MODE_TYPE, - // TODO: Feature flag - createContext, - createElement: __DEV__ ? createElementWithValidation : createElement, cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement, createFactory: __DEV__ ? createFactoryWithValidation : createFactory, @@ -59,6 +57,10 @@ const React = { }, }; +if (enableNewContextAPI) { + React.unstable_createContext = createContext; +} + if (__DEV__) { Object.assign(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, { // These should not be included in production. diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index a84b176d3818e..83b07a03f8c32 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -20,6 +20,8 @@ export const enableMutatingReconciler = true; export const enableNoopReconciler = false; // Experimental persistent mode (Fabric): export const enablePersistentReconciler = false; +// Support for new context API +export const enableNewContextAPI = false; // Helps identify side effects in begin-phase lifecycle hooks and setState reducers: export const debugRenderPhaseSideEffects = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fabric.js b/packages/shared/forks/ReactFeatureFlags.native-fabric.js index 21222cbf4069c..689df2c13c5db 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fabric.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fabric.js @@ -17,6 +17,7 @@ export const enableAsyncSubtreeAPI = true; export const enableCreateRoot = false; export const enableUserTimingAPI = __DEV__; export const warnAboutDeprecatedLifecycles = false; +export const enableNewContextAPI = false; // React Fabric uses persistent reconciler. export const enableMutatingReconciler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native.js b/packages/shared/forks/ReactFeatureFlags.native.js index df48a0a70073a..38c6675c95767 100644 --- a/packages/shared/forks/ReactFeatureFlags.native.js +++ b/packages/shared/forks/ReactFeatureFlags.native.js @@ -25,6 +25,7 @@ export const enableUserTimingAPI = __DEV__; export const enableMutatingReconciler = true; export const enableNoopReconciler = false; export const enablePersistentReconciler = false; +export const enableNewContextAPI = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 6c59c6708e4b2..23eb86bfab56b 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -19,6 +19,7 @@ export const { // The rest of the flags are static for better dead code elimination. export const enableAsyncSubtreeAPI = true; export const enableCreateRoot = true; +export const enableNewContextAPI = true; // The www bundles only use the mutating reconciler. export const enableMutatingReconciler = true; From 59c67e95561c31d104ca158d97edb1f9750dbe95 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 13 Dec 2017 19:11:07 -0800 Subject: [PATCH 09/17] Store nearest provider on context object --- .../src/server/ReactPartialRenderer.js | 29 +++++++++---------- .../src/ReactFiberBeginWork.js | 4 +-- .../src/ReactFiberNewContext.js | 21 +++++++------- packages/shared/ReactContext.js | 1 + packages/shared/ReactTypes.js | 3 +- 5 files changed, 28 insertions(+), 30 deletions(-) diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 76d761f8311a0..ded732e859645 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -125,33 +125,30 @@ if (__DEV__) { } // Context (new API) -let providerStack: Array> = []; // Stack of provider objects +let providerStack: Array> = []; // Stack of provider objects let index = -1; export function pushProvider(provider: ReactProvider): void { index += 1; providerStack[index] = provider; + const context: ReactContext = provider.type.context; + context.currentProvider = provider; } export function popProvider(provider: ReactProvider): void { if (__DEV__) { warning(index > -1 && provider === providerStack[index], 'Unexpected pop.'); } + // $FlowFixMe - Intentionally unsound providerStack[index] = null; index -= 1; -} - -// Find the nearest matching provider -export function getProvider( - context: ReactContext, -): ReactProvider | null { - for (let i = index; i > -1; i--) { - const provider = providerStack[i]; - if (provider.type.context === context) { - return provider; - } + const context: ReactContext = provider.type.context; + if (index < 0) { + context.currentProvider = null; + } else { + const previousProvider = providerStack[index]; + context.currentProvider = previousProvider; } - return null; } let didWarnDefaultInputValue = false; @@ -823,7 +820,7 @@ class ReactDOMServerRenderer { if (typeof elementType === 'object' && elementType !== null) { switch (elementType.$$typeof) { case REACT_PROVIDER_TYPE: { - const provider: ReactProvider = nextChild; + const provider: ReactProvider = (nextChild: any); const nextProps = provider.props; const nextChildren = toArray(nextProps.children); const frame: Frame = { @@ -844,10 +841,10 @@ class ReactDOMServerRenderer { return ''; } case REACT_CONSUMER_TYPE: { - const consumer: ReactConsumer = nextChild; + const consumer: ReactConsumer = (nextChild: any); const nextProps = consumer.props; - const provider = getProvider(consumer.type.context); + const provider = consumer.type.context.currentProvider; let nextValue; if (provider === null) { // Detached consumer diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index e822d00f540c7..429438831c5b1 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -67,7 +67,7 @@ import { pushTopLevelContextObject, invalidateContextProvider, } from './ReactFiberContext'; -import {pushProvider, getProvider} from './ReactFiberNewContext'; +import {pushProvider} from './ReactFiberNewContext'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncUpdates} from './ReactTypeOfInternalContext'; @@ -807,7 +807,7 @@ export default function( const oldProps = workInProgress.memoizedProps; // Get the nearest ancestor provider. - const providerFiber: Fiber | null = getProvider(context); + const providerFiber: Fiber | null = context.currentProvider; let newValue; let valueDidChange; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index bf6a24943c63e..786b1b902bda2 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -18,6 +18,8 @@ let index = -1; export function pushProvider(providerFiber: Fiber): void { index += 1; stack[index] = providerFiber; + const context: ReactContext = providerFiber.type.context; + context.currentProvider = providerFiber; } export function popProvider(providerFiber: Fiber): void { @@ -26,21 +28,20 @@ export function popProvider(providerFiber: Fiber): void { } stack[index] = null; index -= 1; -} - -// Find the nearest matching provider -export function getProvider(context: ReactContext): Fiber | null { - for (let i = index; i > -1; i--) { - const provider = stack[i]; - if (provider.type.context === context) { - return provider; - } + const context: ReactContext = providerFiber.type.context; + if (index < 0) { + context.currentProvider = null; + } else { + const previousProviderFiber = stack[index]; + context.currentProvider = previousProviderFiber; } - return null; } export function resetProviderStack(): void { for (let i = index; i > -1; i--) { + const providerFiber = stack[i]; + const context: ReactContext = providerFiber.type.context; + context.currentProvider = null; stack[i] = null; } } diff --git a/packages/shared/ReactContext.js b/packages/shared/ReactContext.js index 52cf550985bfe..f1af0371bdf53 100644 --- a/packages/shared/ReactContext.js +++ b/packages/shared/ReactContext.js @@ -53,6 +53,7 @@ export function createContext(defaultValue: T): ReactContext { }; }, defaultValue, + currentProvider: null, }; providerType = { diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 8d9faf94be125..e6ef0332a1665 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -1,5 +1,3 @@ -import {Symbol} from './node_modules/typescript/lib/typescript'; - /** * Copyright (c) 2014-present, Facebook, Inc. * @@ -88,6 +86,7 @@ export type ReactContext = { provide(value: T, children: ReactNodeList, key?: string): ReactProvider, consume(render: (value: T) => ReactNodeList, key?: string): ReactConsumer, defaultValue: T, + currentProvider: any, // Fiber | null }; export type ReactPortal = { From 7994d5ac8c0bb4210b7a40b3ed3821a65f0f4033 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 14 Dec 2017 11:14:08 -0800 Subject: [PATCH 10/17] Handle reentrancy in server renderer Context stack should be per server renderer instance. --- .../ReactServerRendering-test.internal.js | 53 +++++++++++++++ .../src/server/ReactPartialRenderer.js | 65 ++++++++++--------- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js index 09a150755536a..ad351544f1253 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js @@ -427,6 +427,59 @@ describe('ReactDOMServer', () => { expect(results).toEqual([2, 1, 3, 1]); }); + it('renders context API, reentrancy', () => { + const Context = React.unstable_createContext(0); + + function Provider(props) { + return Context.provide(props.value, props.children); + } + + function Consumer(props) { + return Context.consume(value => { + return 'Result: ' + value; + }); + } + + let reentrantMarkup; + function Reentrant() { + reentrantMarkup = ReactDOMServer.renderToString( + , + ); + return null; + } + + const Indirection = React.Fragment; + + function App(props) { + return ( + + {props.reentrant && } + + + + + + + + + + + + + + ); + } + + const markup = ReactDOMServer.renderToString( + , + ); + // Extract the numbers rendered by the consumers + const results = markup.match(/\d+/g).map(Number); + const reentrantResults = reentrantMarkup.match(/\d+/g).map(Number); + expect(results).toEqual([2, 1, 3, 1]); + expect(reentrantResults).toEqual([2, 1, 3, 1]); + }); + it('renders components with different batching strategies', () => { class StaticComponent extends React.Component { render() { diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index ded732e859645..c219c04739230 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -124,33 +124,6 @@ if (__DEV__) { }; } -// Context (new API) -let providerStack: Array> = []; // Stack of provider objects -let index = -1; - -export function pushProvider(provider: ReactProvider): void { - index += 1; - providerStack[index] = provider; - const context: ReactContext = provider.type.context; - context.currentProvider = provider; -} - -export function popProvider(provider: ReactProvider): void { - if (__DEV__) { - warning(index > -1 && provider === providerStack[index], 'Unexpected pop.'); - } - // $FlowFixMe - Intentionally unsound - providerStack[index] = null; - index -= 1; - const context: ReactContext = provider.type.context; - if (index < 0) { - context.currentProvider = null; - } else { - const previousProvider = providerStack[index]; - context.currentProvider = previousProvider; - } -} - let didWarnDefaultInputValue = false; let didWarnDefaultChecked = false; let didWarnDefaultSelectValue = false; @@ -657,6 +630,9 @@ class ReactDOMServerRenderer { previousWasTextNode: boolean; makeStaticMarkup: boolean; + providerStack: Array>; + providerIndex: number; + constructor(children: mixed, makeStaticMarkup: boolean) { const flatChildren = flattenTopLevelChildren(children); @@ -678,6 +654,37 @@ class ReactDOMServerRenderer { this.currentSelectValue = null; this.previousWasTextNode = false; this.makeStaticMarkup = makeStaticMarkup; + + // Context (new API) + this.providerStack = []; // Stack of provider objects + this.providerIndex = -1; + } + + pushProvider(provider: ReactProvider): void { + this.providerIndex += 1; + this.providerStack[this.providerIndex] = provider; + const context: ReactContext = provider.type.context; + context.currentProvider = provider; + } + + popProvider(provider: ReactProvider): void { + if (__DEV__) { + warning( + this.providerIndex > -1 && + provider === this.providerStack[this.providerIndex], + 'Unexpected pop.', + ); + } + // $FlowFixMe - Intentionally unsound + this.providerStack[this.providerIndex] = null; + this.providerIndex -= 1; + const context: ReactContext = provider.type.context; + if (this.providerIndex < 0) { + context.currentProvider = null; + } else { + const previousProvider = this.providerStack[this.providerIndex]; + context.currentProvider = previousProvider; + } } read(bytes: number): string | null { @@ -707,7 +714,7 @@ class ReactDOMServerRenderer { frame.type.type.$$typeof === REACT_PROVIDER_TYPE ) { const provider: ReactProvider = (frame.type: any); - popProvider(provider); + this.popProvider(provider); } continue; } @@ -835,7 +842,7 @@ class ReactDOMServerRenderer { ((frame: any): FrameDev).debugElementStack = []; } - pushProvider(provider); + this.pushProvider(provider); this.stack.push(frame); return ''; From bfebf36105bb01612aa18182af2c79d0d905e322 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 14 Dec 2017 18:23:14 -0800 Subject: [PATCH 11/17] Bailout of consumer updates using bitmask The context type defines an optional function that compares two context values, returning a bitfield. A consumer may specify the bits it needs for rendering. If a provider's context changes, and the consumer's bits do not intersect with the changed bits, we can skip the consumer. This is similar to how selectors are used in Redux but fast enough to do while scanning the tree. The only user code involved is the function that computes the changed bits. But that's only called once per provider update, not for every consumer. --- .../src/server/ReactPartialRenderer.js | 8 +- packages/react-reconciler/src/ReactFiber.js | 5 +- .../src/ReactFiberBeginWork.js | 89 +++++++++++-------- .../src/ReactFiberExpirationTime.js | 4 +- .../ReactNewContext-test.internal.js | 75 ++++++++++++++++ .../react-reconciler/src/maxSigned32BitInt.js | 13 +++ packages/shared/ReactContext.js | 35 ++++++-- packages/shared/ReactSymbols.js | 4 +- packages/shared/ReactTypes.js | 18 ++-- 9 files changed, 187 insertions(+), 64 deletions(-) create mode 100644 packages/react-reconciler/src/maxSigned32BitInt.js diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index c219c04739230..a4a29a323ece4 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -31,7 +31,7 @@ import { REACT_RETURN_TYPE, REACT_PORTAL_TYPE, REACT_PROVIDER_TYPE, - REACT_CONSUMER_TYPE, + REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; import { @@ -847,15 +847,15 @@ class ReactDOMServerRenderer { this.stack.push(frame); return ''; } - case REACT_CONSUMER_TYPE: { + case REACT_CONTEXT_TYPE: { const consumer: ReactConsumer = (nextChild: any); const nextProps = consumer.props; - const provider = consumer.type.context.currentProvider; + const provider = consumer.type.currentProvider; let nextValue; if (provider === null) { // Detached consumer - nextValue = consumer.type.context.defaultValue; + nextValue = consumer.type.defaultValue; } else { nextValue = provider.props.value; } diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 5690b52679e69..74d951cf7921b 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -44,7 +44,7 @@ import { REACT_CALL_TYPE, REACT_STRICT_MODE_TYPE, REACT_PROVIDER_TYPE, - REACT_CONSUMER_TYPE, + REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -352,7 +352,8 @@ export function createFiberFromElement( case REACT_PROVIDER_TYPE: fiberTag = ProviderComponent; break; - case REACT_CONSUMER_TYPE: + case REACT_CONTEXT_TYPE: + // This is a consumer fiberTag = ConsumerComponent; break; default: diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 429438831c5b1..c0625973bd5f4 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -8,11 +8,7 @@ */ import type {HostConfig} from 'react-reconciler'; -import type { - ReactProviderType, - ReactConsumerType, - ReactContext, -} from 'shared/ReactTypes'; +import type {ReactProviderType, ReactContext} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {HostContext} from './ReactFiberHostContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; @@ -70,6 +66,7 @@ import { import {pushProvider} from './ReactFiberNewContext'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncUpdates} from './ReactTypeOfInternalContext'; +import MAX_SIGNED_32_BIT_INT from './maxSigned32BitInt'; let didWarnAboutBadClass; let didWarnAboutGetDerivedStateOnFunctionalComponent; @@ -668,6 +665,7 @@ export default function( function propagateContextChange( workInProgress: Fiber, context: ReactContext, + changedBits: number, renderExpirationTime: ExpirationTime, ): void { if (enableNewContextAPI) { @@ -678,7 +676,8 @@ export default function( switch (fiber.tag) { case ConsumerComponent: // Check if the context matches. - if (fiber.type.context === context) { + const bits = fiber.stateNode; + if (fiber.type === context && (bits & changedBits) !== 0) { // Update the expiration time of all the ancestors, including // the alternates. let node = fiber; @@ -720,7 +719,7 @@ export default function( break; case ProviderComponent: // Don't scan deeper if this is a matching provider - nextFiber = fiber.type === context ? null : fiber.child; + nextFiber = fiber.type === workInProgress.type ? null : fiber.child; break; default: // Traverse down. @@ -776,12 +775,33 @@ export default function( workInProgress.memoizedProps = newProps; const newValue = newProps.value; - const oldValue = oldProps !== null ? oldProps.value : null; - // Use Object.is to compare the new context value to the old value. - if (!is(newValue, oldValue)) { - propagateContextChange(workInProgress, context, renderExpirationTime); + let changedBits: number; + if (oldProps === null) { + // Initial render + changedBits = MAX_SIGNED_32_BIT_INT; + } else { + const oldValue = oldProps.value; + // Use Object.is to compare the new context value to the old value. + if (!is(newValue, oldValue)) { + changedBits = + context.calculateChangedBits !== null + ? context.calculateChangedBits(oldValue, newValue) + : MAX_SIGNED_32_BIT_INT; + if (changedBits !== 0) { + propagateContextChange( + workInProgress, + context, + changedBits, + renderExpirationTime, + ); + } + } else { + // No change. + changedBits = 0; + } } + workInProgress.stateNode = changedBits; if (oldProps !== null && oldProps.children === newProps.children) { return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -800,8 +820,7 @@ export default function( renderExpirationTime, ) { if (enableNewContextAPI) { - const consumerType: ReactConsumerType = workInProgress.type; - const context: ReactContext = consumerType.context; + const context: ReactContext = workInProgress.type; const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; @@ -810,12 +829,12 @@ export default function( const providerFiber: Fiber | null = context.currentProvider; let newValue; - let valueDidChange; + let changedBits; if (providerFiber === null) { // This is a detached consumer (has no provider). Use the default // context value. newValue = context.defaultValue; - valueDidChange = false; + changedBits = 0; } else { const provider = providerFiber.pendingProps; invariant( @@ -824,35 +843,31 @@ export default function( 'a bug in React. Please file an issue.', ); newValue = provider.value; - - // Context change propagation stops at matching consumers, for time- - // slicing. Continue the propagation here. - if (oldProps === null) { - valueDidChange = true; - propagateContextChange(workInProgress, context, renderExpirationTime); - } else { - const oldValue = oldProps !== null ? oldProps.__memoizedValue : null; - // Use Object.is to compare the new context value to the old value. - if (!is(newValue, oldValue)) { - valueDidChange = true; - propagateContextChange( - workInProgress, - context, - renderExpirationTime, - ); - } + changedBits = providerFiber.stateNode; + if (changedBits !== 0) { + // Context change propagation stops at matching consumers, for time- + // slicing. Continue the propagation here. + propagateContextChange( + workInProgress, + context, + changedBits, + renderExpirationTime, + ); } } - // The old context value is stored on the consumer object. We can't use the - // provider's memoizedProps because those have already been updated by the - // time we get here, in the provider's begin phase. - newProps.__memoizedValue = newValue; + // Store the bits on the fiber's stateNode for quick access. + let bits = newProps.bits; + if (bits === undefined || bits === null) { + // Subscribe to all changes by default + bits = MAX_SIGNED_32_BIT_INT; + } + workInProgress.stateNode = bits; 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 (newProps === oldProps && !valueDidChange) { + } else if (newProps === oldProps && changedBits === 0) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } const newChildren = newProps.render(newValue); diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index 182740f89584e..5d8c93b07cec7 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -7,12 +7,14 @@ * @flow */ +import MAX_SIGNED_32_BIT_INT from './maxSigned32BitInt'; + // TODO: Use an opaque type once ESLint et al support the syntax export type ExpirationTime = number; export const NoWork = 0; export const Sync = 1; -export const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1 +export const Never = MAX_SIGNED_32_BIT_INT; const UNIT_SIZE = 10; const MAGIC_NUMBER_OFFSET = 2; diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 233cc12e6823e..5bad4ebc726d3 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -425,6 +425,80 @@ describe('ReactNewContext', () => { ]); }); + it('can skip consumers with bitmask', () => { + const Context = React.unstable_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 Context.provide({foo: props.foo, bar: props.bar}, props.children); + } + + function Foo() { + return Context.consume(value => { + ReactNoop.yield('Foo'); + return ; + }, 0b01); + } + + function Bar() { + return Context.consume(value => { + ReactNoop.yield('Bar'); + return ; + }, 0b10); + } + + 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')]); + }); + describe('fuzz test', () => { const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; const contexts = new Map( @@ -521,6 +595,7 @@ describe('ReactNewContext', () => { /> ), + null, i, ); }); diff --git a/packages/react-reconciler/src/maxSigned32BitInt.js b/packages/react-reconciler/src/maxSigned32BitInt.js new file mode 100644 index 0000000000000..9609a161117e6 --- /dev/null +++ b/packages/react-reconciler/src/maxSigned32BitInt.js @@ -0,0 +1,13 @@ +/** + * 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 + */ + +// The maximum safe integer for bitwise operations. +// Math.pow(2, 31) - 1 +// 0b1111111111111111111111111111111 +export default 2147483647; diff --git a/packages/shared/ReactContext.js b/packages/shared/ReactContext.js index f1af0371bdf53..a1c11e66dc916 100644 --- a/packages/shared/ReactContext.js +++ b/packages/shared/ReactContext.js @@ -9,7 +9,7 @@ import { REACT_PROVIDER_TYPE, - REACT_CONSUMER_TYPE, + REACT_CONTEXT_TYPE, REACT_ELEMENT_TYPE, } from 'shared/ReactSymbols'; @@ -20,11 +20,30 @@ import type { ReactNodeList, } from 'shared/ReactTypes'; -export function createContext(defaultValue: T): ReactContext { +import warning from 'fbjs/lib/warning'; + +export function createContext( + defaultValue: T, + calculateChangedBits: ?(a: T, b: T) => number, +): ReactContext { let providerType; - let consumerType; + + if (calculateChangedBits === undefined) { + calculateChangedBits = null; + } else { + if (__DEV__) { + warning( + calculateChangedBits === null || + typeof calculateChangedBits === 'function', + 'createContext: Expected the optional second argument to be a ' + + 'function. Instead received: %s', + calculateChangedBits, + ); + } + } const context = { + $$typeof: REACT_CONTEXT_TYPE, provide(value: T, children: ReactNodeList, key?: string): ReactProvider { return { $$typeof: REACT_ELEMENT_TYPE, @@ -39,20 +58,22 @@ export function createContext(defaultValue: T): ReactContext { }, consume( render: (value: T) => ReactNodeList, + bits?: number, key?: string, ): ReactConsumer { return { $$typeof: REACT_ELEMENT_TYPE, - type: consumerType, + type: context, key: key === null || key === undefined ? null : '' + key, ref: null, props: { + bits, render, - __memoizedValue: null, }, }; }, defaultValue, + calculateChangedBits, currentProvider: null, }; @@ -60,10 +81,6 @@ export function createContext(defaultValue: T): ReactContext { $$typeof: REACT_PROVIDER_TYPE, context, }; - consumerType = { - $$typeof: REACT_CONSUMER_TYPE, - context, - }; return context; } diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 3398d987ca718..037d4a38d0f39 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -30,8 +30,8 @@ export const REACT_STRICT_MODE_TYPE = hasSymbol export const REACT_PROVIDER_TYPE = hasSymbol ? Symbol.for('react.provider') : 0xeacd; -export const REACT_CONSUMER_TYPE = hasSymbol - ? Symbol.for('react.consumer') +export const REACT_CONTEXT_TYPE = hasSymbol + ? Symbol.for('react.context') : 0xeace; const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index e6ef0332a1665..2d1392d30bc1f 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -67,25 +67,25 @@ export type ReactProviderType = { export type ReactConsumer = { $$typeof: Symbol | number, - type: ReactConsumerType, + type: ReactContext, key: null | string, ref: null, props: { render: (value: T) => ReactNodeList, - // TODO: Remove this hack - __memoizedValue: T | null, + bits?: number, }, }; -export type ReactConsumerType = { - $$typeof: Symbol | number, - context: ReactContext, -}; - export type ReactContext = { + $$typeof: Symbol | number, provide(value: T, children: ReactNodeList, key?: string): ReactProvider, - consume(render: (value: T) => ReactNodeList, key?: string): ReactConsumer, + consume( + render: (value: T) => ReactNodeList, + bits?: number, + key?: string, + ): ReactConsumer, defaultValue: T, + calculateChangedBits: ((a: T, b: T) => number) | null, currentProvider: any, // Fiber | null }; From f39333e3d142d5b67d30bb88aa9152cec3f19725 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 15 Dec 2017 03:46:57 -0800 Subject: [PATCH 12/17] Store current value and changed bits on context object There are fewer providers than consumers, so better to do this work at the provider. --- .../src/server/ReactPartialRenderer.js | 16 ++---- .../src/ReactFiberBeginWork.js | 51 +++++-------------- .../src/ReactFiberNewContext.js | 12 +++-- packages/shared/ReactContext.js | 5 +- packages/shared/ReactTypes.js | 5 +- 5 files changed, 32 insertions(+), 57 deletions(-) diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index a4a29a323ece4..3a2e971fe01bd 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -664,7 +664,7 @@ class ReactDOMServerRenderer { this.providerIndex += 1; this.providerStack[this.providerIndex] = provider; const context: ReactContext = provider.type.context; - context.currentProvider = provider; + context.currentValue = provider.props.value; } popProvider(provider: ReactProvider): void { @@ -680,10 +680,10 @@ class ReactDOMServerRenderer { this.providerIndex -= 1; const context: ReactContext = provider.type.context; if (this.providerIndex < 0) { - context.currentProvider = null; + context.currentValue = context.defaultValue; } else { const previousProvider = this.providerStack[this.providerIndex]; - context.currentProvider = previousProvider; + context.currentValue = previousProvider.props.value; } } @@ -850,15 +850,7 @@ class ReactDOMServerRenderer { case REACT_CONTEXT_TYPE: { const consumer: ReactConsumer = (nextChild: any); const nextProps = consumer.props; - - const provider = consumer.type.currentProvider; - let nextValue; - if (provider === null) { - // Detached consumer - nextValue = consumer.type.defaultValue; - } else { - nextValue = provider.props.value; - } + const nextValue = consumer.type.currentValue; const nextChildren = toArray(nextProps.render(nextValue)); const frame: Frame = { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index c0625973bd5f4..5fbe61cb90635 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -764,12 +764,12 @@ export default function( const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - pushProvider(workInProgress); - if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed // we don't do the bailout and we have to reuse existing props instead. } else if (oldProps === newProps) { + pushProvider(workInProgress); + workInProgress.stateNode = 0; return bailoutOnAlreadyFinishedWork(current, workInProgress); } workInProgress.memoizedProps = newProps; @@ -801,7 +801,9 @@ export default function( changedBits = 0; } } + workInProgress.stateNode = changedBits; + pushProvider(workInProgress); if (oldProps !== null && oldProps.children === newProps.children) { return bailoutOnAlreadyFinishedWork(current, workInProgress); @@ -821,39 +823,20 @@ export default function( ) { if (enableNewContextAPI) { const context: ReactContext = workInProgress.type; - const newProps = workInProgress.pendingProps; - const oldProps = workInProgress.memoizedProps; - // Get the nearest ancestor provider. - const providerFiber: Fiber | null = context.currentProvider; + const newValue = context.currentValue; + const changedBits = context.changedBits; - let newValue; - let changedBits; - if (providerFiber === null) { - // This is a detached consumer (has no provider). Use the default - // context value. - newValue = context.defaultValue; - changedBits = 0; - } else { - const provider = providerFiber.pendingProps; - invariant( - provider, - 'Provider should have pending props. This error is likely caused by ' + - 'a bug in React. Please file an issue.', + if (changedBits !== 0) { + // Context change propagation stops at matching consumers, for time- + // slicing. Continue the propagation here. + propagateContextChange( + workInProgress, + context, + changedBits, + renderExpirationTime, ); - newValue = provider.value; - changedBits = providerFiber.stateNode; - if (changedBits !== 0) { - // Context change propagation stops at matching consumers, for time- - // slicing. Continue the propagation here. - propagateContextChange( - workInProgress, - context, - changedBits, - renderExpirationTime, - ); - } } // Store the bits on the fiber's stateNode for quick access. @@ -864,12 +847,6 @@ export default function( } workInProgress.stateNode = bits; - 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 (newProps === oldProps && changedBits === 0) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } const newChildren = newProps.render(newValue); reconcileChildren(current, workInProgress, newChildren); return workInProgress.child; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 786b1b902bda2..7f61354e0d400 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -19,7 +19,8 @@ export function pushProvider(providerFiber: Fiber): void { index += 1; stack[index] = providerFiber; const context: ReactContext = providerFiber.type.context; - context.currentProvider = providerFiber; + context.currentValue = providerFiber.pendingProps.value; + context.changedBits = providerFiber.stateNode; } export function popProvider(providerFiber: Fiber): void { @@ -30,10 +31,12 @@ export function popProvider(providerFiber: Fiber): void { index -= 1; const context: ReactContext = providerFiber.type.context; if (index < 0) { - context.currentProvider = null; + context.currentValue = context.defaultValue; + context.changedBits = 0; } else { const previousProviderFiber = stack[index]; - context.currentProvider = previousProviderFiber; + context.currentValue = previousProviderFiber.pendingProps.value; + context.changedBits = previousProviderFiber.stateNode; } } @@ -41,7 +44,8 @@ export function resetProviderStack(): void { for (let i = index; i > -1; i--) { const providerFiber = stack[i]; const context: ReactContext = providerFiber.type.context; - context.currentProvider = null; + context.currentValue = context.defaultValue; + context.changedBits = 0; stack[i] = null; } } diff --git a/packages/shared/ReactContext.js b/packages/shared/ReactContext.js index a1c11e66dc916..c8aa813897c1c 100644 --- a/packages/shared/ReactContext.js +++ b/packages/shared/ReactContext.js @@ -72,9 +72,10 @@ export function createContext( }, }; }, - defaultValue, calculateChangedBits, - currentProvider: null, + defaultValue, + currentValue: defaultValue, + changedBits: 0, }; providerType = { diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 2d1392d30bc1f..198675ea8ebcc 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -84,9 +84,10 @@ export type ReactContext = { bits?: number, key?: string, ): ReactConsumer, - defaultValue: T, calculateChangedBits: ((a: T, b: T) => number) | null, - currentProvider: any, // Fiber | null + defaultValue: T, + currentValue: T, + changedBits: number, }; export type ReactPortal = { From b7fec0529c10397304982ce1188e27ed5c51006d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 15 Dec 2017 04:25:05 -0800 Subject: [PATCH 13/17] Use maximum of 31 bits for bitmask This is the largest integer size in V8 on 32-bit systems. Warn in development if too large a number is used. --- .../src/ReactFiberBeginWork.js | 30 ++++++++++++------- .../src/ReactFiberExpirationTime.js | 4 +-- .../ReactNewContext-test.internal.js | 28 +++++++++++++++++ ...Signed32BitInt.js => maxSigned31BitInt.js} | 8 ++--- packages/shared/ReactContext.js | 4 +-- packages/shared/ReactTypes.js | 2 +- 6 files changed, 57 insertions(+), 19 deletions(-) rename packages/react-reconciler/src/{maxSigned32BitInt.js => maxSigned31BitInt.js} (55%) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 5fbe61cb90635..dd99d9d0aed44 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -66,7 +66,7 @@ import { import {pushProvider} from './ReactFiberNewContext'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncUpdates} from './ReactTypeOfInternalContext'; -import MAX_SIGNED_32_BIT_INT from './maxSigned32BitInt'; +import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; let didWarnAboutBadClass; let didWarnAboutGetDerivedStateOnFunctionalComponent; @@ -676,8 +676,8 @@ export default function( switch (fiber.tag) { case ConsumerComponent: // Check if the context matches. - const bits = fiber.stateNode; - if (fiber.type === context && (bits & changedBits) !== 0) { + 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; @@ -779,7 +779,7 @@ export default function( let changedBits: number; if (oldProps === null) { // Initial render - changedBits = MAX_SIGNED_32_BIT_INT; + changedBits = MAX_SIGNED_31_BIT_INT; } else { const oldValue = oldProps.value; // Use Object.is to compare the new context value to the old value. @@ -787,7 +787,17 @@ export default function( changedBits = context.calculateChangedBits !== null ? context.calculateChangedBits(oldValue, newValue) - : MAX_SIGNED_32_BIT_INT; + : MAX_SIGNED_31_BIT_INT; + if (__DEV__) { + warning( + (changedBits & MAX_SIGNED_31_BIT_INT) === changedBits, + 'calculateChangedBits: Expected the return value to be a ' + + '31-bit integer. Instead received: %s', + changedBits, + ); + } + changedBits |= 0; + if (changedBits !== 0) { propagateContextChange( workInProgress, @@ -839,13 +849,13 @@ export default function( ); } - // Store the bits on the fiber's stateNode for quick access. - let bits = newProps.bits; - if (bits === undefined || bits === null) { + // Store the observedBits on the fiber's stateNode for quick access. + let observedBits = newProps.observedBits; + if (observedBits === undefined || observedBits === null) { // Subscribe to all changes by default - bits = MAX_SIGNED_32_BIT_INT; + observedBits = MAX_SIGNED_31_BIT_INT; } - workInProgress.stateNode = bits; + workInProgress.stateNode = observedBits; const newChildren = newProps.render(newValue); reconcileChildren(current, workInProgress, newChildren); diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index 5d8c93b07cec7..dca42c4e3db2f 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -7,14 +7,14 @@ * @flow */ -import MAX_SIGNED_32_BIT_INT from './maxSigned32BitInt'; +import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; // TODO: Use an opaque type once ESLint et al support the syntax export type ExpirationTime = number; export const NoWork = 0; export const Sync = 1; -export const Never = MAX_SIGNED_32_BIT_INT; +export const Never = MAX_SIGNED_31_BIT_INT; const UNIT_SIZE = 10; const MAGIC_NUMBER_OFFSET = 2; diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 5bad4ebc726d3..9e52bc4b0dc57 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -499,6 +499,34 @@ describe('ReactNewContext', () => { expect(ReactNoop.getChildren()).toEqual([span('Foo: 3'), span('Bar: 3')]); }); + it('warns if calculateChangedBits returns larger than a 31-bit integer', () => { + spyOnDev(console, 'error'); + + const Context = React.unstable_createContext( + 0, + (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int + ); + + function Provider(props) { + return Context.provide(props.value, props.children); + } + + ReactNoop.render(); + ReactNoop.flush(); + + // Update + ReactNoop.render(); + ReactNoop.flush(); + + if (__DEV__) { + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'calculateChangedBits: Expected the return value to be a 31-bit ' + + 'integer. Instead received: 4294967295', + ); + } + }); + describe('fuzz test', () => { const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; const contexts = new Map( diff --git a/packages/react-reconciler/src/maxSigned32BitInt.js b/packages/react-reconciler/src/maxSigned31BitInt.js similarity index 55% rename from packages/react-reconciler/src/maxSigned32BitInt.js rename to packages/react-reconciler/src/maxSigned31BitInt.js index 9609a161117e6..2b6e167d0dfe7 100644 --- a/packages/react-reconciler/src/maxSigned32BitInt.js +++ b/packages/react-reconciler/src/maxSigned31BitInt.js @@ -7,7 +7,7 @@ * @flow */ -// The maximum safe integer for bitwise operations. -// Math.pow(2, 31) - 1 -// 0b1111111111111111111111111111111 -export default 2147483647; +// Max 31 bit integer. The max integer size in V8 for 32-bit systems. +// Math.pow(2, 30) - 1 +// 0b111111111111111111111111111111 +export default 1073741823; diff --git a/packages/shared/ReactContext.js b/packages/shared/ReactContext.js index c8aa813897c1c..7dde87be3e774 100644 --- a/packages/shared/ReactContext.js +++ b/packages/shared/ReactContext.js @@ -58,7 +58,7 @@ export function createContext( }, consume( render: (value: T) => ReactNodeList, - bits?: number, + observedBits?: number, key?: string, ): ReactConsumer { return { @@ -67,7 +67,7 @@ export function createContext( key: key === null || key === undefined ? null : '' + key, ref: null, props: { - bits, + observedBits, render, }, }; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 198675ea8ebcc..062fa6f2d0155 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -81,7 +81,7 @@ export type ReactContext = { provide(value: T, children: ReactNodeList, key?: string): ReactProvider, consume( render: (value: T) => ReactNodeList, - bits?: number, + observedBits?: number, key?: string, ): ReactConsumer, calculateChangedBits: ((a: T, b: T) => number) | null, From e67bfd847f3e6b7cde0ff17d534d7e61230e0639 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 15 Dec 2017 04:32:11 -0800 Subject: [PATCH 14/17] ProviderComponent -> ContextProvider, ConsumerComponent -> ContextConsumer --- packages/react-reconciler/src/ReactFiber.js | 8 +++---- .../src/ReactFiberBeginWork.js | 22 +++++++++---------- .../src/ReactFiberCompleteWork.js | 8 +++---- .../src/ReactFiberScheduler.js | 4 ++-- packages/shared/ReactTypeOfWork.js | 4 ++-- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 74d951cf7921b..fb6ba80555f2a 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -27,8 +27,8 @@ import { ReturnComponent, Fragment, Mode, - ProviderComponent, - ConsumerComponent, + ContextProvider, + ContextConsumer, } from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; @@ -350,11 +350,11 @@ export function createFiberFromElement( if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { case REACT_PROVIDER_TYPE: - fiberTag = ProviderComponent; + fiberTag = ContextProvider; break; case REACT_CONTEXT_TYPE: // This is a consumer - fiberTag = ConsumerComponent; + fiberTag = ContextConsumer; break; default: if (typeof type.tag === 'number') { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index dd99d9d0aed44..91d1034342397 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -29,8 +29,8 @@ import { ReturnComponent, Fragment, Mode, - ProviderComponent, - ConsumerComponent, + ContextProvider, + ContextConsumer, } from 'shared/ReactTypeOfWork'; import { PerformedWork, @@ -674,7 +674,7 @@ export default function( let nextFiber; // Visit this fiber. switch (fiber.tag) { - case ConsumerComponent: + case ContextConsumer: // Check if the context matches. const observedBits: number = fiber.stateNode | 0; if (fiber.type === context && (observedBits & changedBits) !== 0) { @@ -717,7 +717,7 @@ export default function( nextFiber = fiber.child; } break; - case ProviderComponent: + case ContextProvider: // Don't scan deeper if this is a matching provider nextFiber = fiber.type === workInProgress.type ? null : fiber.child; break; @@ -752,7 +752,7 @@ export default function( } } - function updateProviderComponent( + function updateContextProvider( current, workInProgress, renderExpirationTime, @@ -826,7 +826,7 @@ export default function( } } - function updateConsumerComponent( + function updateContextConsumer( current, workInProgress, renderExpirationTime, @@ -926,7 +926,7 @@ export default function( workInProgress.stateNode.containerInfo, ); break; - case ProviderComponent: + case ContextProvider: pushProvider(workInProgress); break; } @@ -1007,14 +1007,14 @@ export default function( return updateFragment(current, workInProgress); case Mode: return updateMode(current, workInProgress); - case ProviderComponent: - return updateProviderComponent( + case ContextProvider: + return updateContextProvider( current, workInProgress, renderExpirationTime, ); - case ConsumerComponent: - return updateConsumerComponent( + case ContextConsumer: + return updateContextConsumer( current, workInProgress, renderExpirationTime, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 53db89213da6f..3cf970b844137 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -30,8 +30,8 @@ import { CallComponent, CallHandlerPhase, ReturnComponent, - ProviderComponent, - ConsumerComponent, + ContextProvider, + ContextConsumer, Fragment, Mode, } from 'shared/ReactTypeOfWork'; @@ -586,11 +586,11 @@ export default function( popHostContainer(workInProgress); updateHostContainer(workInProgress); return null; - case ProviderComponent: + case ContextProvider: // Pop provider fiber popProvider(workInProgress); return null; - case ConsumerComponent: + case ContextConsumer: return null; // Error cases case IndeterminateComponent: diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 2991435c1ec3b..e223f2aa26fd6 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -33,7 +33,7 @@ import { HostComponent, HostPortal, ClassComponent, - ProviderComponent, + ContextProvider, } from 'shared/ReactTypeOfWork'; import {enableUserTimingAPI} from 'shared/ReactFeatureFlags'; import getComponentName from 'shared/getComponentName'; @@ -1138,7 +1138,7 @@ export default function( case HostPortal: popHostContainer(node); break; - case ProviderComponent: + case ContextProvider: popProvider(node); break; } diff --git a/packages/shared/ReactTypeOfWork.js b/packages/shared/ReactTypeOfWork.js index 59add8379a9ae..3d1bfc3f37584 100644 --- a/packages/shared/ReactTypeOfWork.js +++ b/packages/shared/ReactTypeOfWork.js @@ -35,5 +35,5 @@ export const CallHandlerPhase = 8; export const ReturnComponent = 9; export const Fragment = 10; export const Mode = 11; -export const ProviderComponent = 12; -export const ConsumerComponent = 13; +export const ContextConsumer = 12; +export const ContextProvider = 13; From 79e289c6c209a209bd13ff094f4132d4896305f4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 15 Dec 2017 04:41:36 -0800 Subject: [PATCH 15/17] Inline Object.is --- .../src/ReactFiberBeginWork.js | 15 ++++++--- packages/shared/is.js | 31 ------------------- 2 files changed, 10 insertions(+), 36 deletions(-) delete mode 100644 packages/shared/is.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 91d1034342397..1dd9f7c0da8ed 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -39,7 +39,6 @@ import { Err, Ref, } from 'shared/ReactTypeOfSideEffect'; -import is from 'shared/is'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; import {debugRenderPhaseSideEffects} from 'shared/ReactFeatureFlags'; import invariant from 'fbjs/lib/invariant'; @@ -783,7 +782,16 @@ export default function( } else { const oldValue = oldProps.value; // Use Object.is to compare the new context value to the old value. - if (!is(newValue, oldValue)) { + // Inlined Object.is polyfill. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + if ( + (oldValue === newValue && + (oldValue !== 0 || 1 / oldValue === 1 / newValue)) || + (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare + ) { + // No change. + changedBits = 0; + } else { changedBits = context.calculateChangedBits !== null ? context.calculateChangedBits(oldValue, newValue) @@ -806,9 +814,6 @@ export default function( renderExpirationTime, ); } - } else { - // No change. - changedBits = 0; } } diff --git a/packages/shared/is.js b/packages/shared/is.js deleted file mode 100644 index 19a0c49cb0006..0000000000000 --- a/packages/shared/is.js +++ /dev/null @@ -1,31 +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 - */ - -let is: (x: mixed, y: mixed) => boolean; - -if (typeof Object.is === 'function') { - is = Object.is; -} else { - // inlined Object.is polyfill to avoid requiring consumers ship their own - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is - is = (x: mixed, y: mixed) => { - // SameValue algorithm - if (x === y) { - // Steps 1-5, 7-10 - // Steps 6.b-6.e: +0 != -0 - // Added the nonzero y check to make Flow happy, but it is redundant - return x !== 0 || y !== 0 || 1 / x === 1 / y; - } else { - // Step 6.a: NaN == NaN - return x !== x && y !== y; // eslint-disable-line no-self-compare - } - }; -} - -export default is; From d2bd03aa328f9ecf65cbbe19d40d02214fcfade2 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 15 Dec 2017 10:56:29 -0800 Subject: [PATCH 16/17] Warn if multiple renderers concurrently render the same context provider Let's see if we can get away with not supporting this for now. If it turns out that it's needed, we can fall back to backtracking the fiber return path. --- .../src/ReactFiberNewContext.js | 19 ++ .../ReactNewContext-test.internal.js | 183 +++++++++++------- packages/shared/ReactContext.js | 6 +- packages/shared/ReactTypes.js | 3 + 4 files changed, 142 insertions(+), 69 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 7f61354e0d400..ba59ae5998b94 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -15,12 +15,28 @@ import warning from 'fbjs/lib/warning'; let stack: Array = []; let index = -1; +let rendererSigil; +if (__DEV__) { + // Use this to detect multiple renderers using the same context + rendererSigil = {}; +} + export function pushProvider(providerFiber: Fiber): void { index += 1; stack[index] = providerFiber; const context: ReactContext = providerFiber.type.context; context.currentValue = providerFiber.pendingProps.value; context.changedBits = providerFiber.stateNode; + + if (__DEV__) { + warning( + context._currentRenderer === null || + context._currentRenderer === rendererSigil, + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + context._currentRenderer = rendererSigil; + } } export function popProvider(providerFiber: Fiber): void { @@ -47,5 +63,8 @@ export function resetProviderStack(): void { context.currentValue = context.defaultValue; context.changedBits = 0; stack[i] = null; + if (__DEV__) { + context._currentRenderer = null; + } } } diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 9e52bc4b0dc57..cf366cf05a43b 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -527,16 +527,53 @@ describe('ReactNewContext', () => { } }); + it('warns if multiple renderers concurrently render the same context', () => { + spyOnDev(console, 'error'); + const Context = React.unstable_createContext(0); + + function Foo(props) { + ReactNoop.yield('Foo'); + return null; + } + function Provider(props) { + return Context.provide(props.value, props.children); + } + + 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'); + ReactFeatureFlags.enableNewContextAPI = true; + 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', + ); + } + }); + describe('fuzz test', () => { - const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; - const contexts = new Map( - contextKeys.map(key => { - const Context = React.unstable_createContext(0); - Context.displayName = 'Context' + key; - return [key, Context]; - }), - ); const Fragment = React.Fragment; + const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; const FLUSH_ALL = 'FLUSH_ALL'; function flushAll() { @@ -600,72 +637,82 @@ describe('ReactNewContext', () => { return actions; } - class ConsumerTree extends React.Component { - shouldComponentUpdate() { - return false; - } - render() { - if (this.props.depth >= this.props.maxDepth) { - return null; + function ContextSimulator(maxDepth) { + const contexts = new Map( + contextKeys.map(key => { + const Context = React.unstable_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 Context.consume( + value => ( + + + + + ), + null, + i, + ); + }); + return consumers; } - const consumers = [0, 1, 2].map(i => { - const randomKey = - contextKeys[this.props.rand.intBetween(0, contextKeys.length - 1)]; - const Context = contexts.get(randomKey); - return Context.consume( - value => ( - - - - - ), - null, - i, - ); - }); - return consumers; } - } - function Root(props) { - return contextKeys.reduceRight((children, key) => { - const Context = contexts.get(key); - const value = props.values[key]; - return Context.provide(value, children); - }, ); - } + function Root(props) { + return contextKeys.reduceRight((children, key) => { + const Context = contexts.get(key); + const value = props.values[key]; + return Context.provide(value, children); + }, ); + } - const initialValues = contextKeys.reduce( - (result, key, i) => ({...result, [key]: i + 1}), - {}, - ); + 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 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 ContextSimulator(maxDepth) { function simulate(seed, actions) { const rand = gen.create(seed); let finalExpectedValues = initialValues; diff --git a/packages/shared/ReactContext.js b/packages/shared/ReactContext.js index 7dde87be3e774..2cc5bd6c43688 100644 --- a/packages/shared/ReactContext.js +++ b/packages/shared/ReactContext.js @@ -42,7 +42,7 @@ export function createContext( } } - const context = { + const context: ReactContext = { $$typeof: REACT_CONTEXT_TYPE, provide(value: T, children: ReactNodeList, key?: string): ReactProvider { return { @@ -83,5 +83,9 @@ export function createContext( context, }; + if (__DEV__) { + context._currentRenderer = null; + } + return context; } diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 062fa6f2d0155..0e29678f35b00 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -88,6 +88,9 @@ export type ReactContext = { defaultValue: T, currentValue: T, changedBits: number, + + // DEV only + _currentRenderer?: Object | null, }; export type ReactPortal = { From 0276eb5c5b27ea6ac251c42bd83211c7d486ab16 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 24 Jan 2018 19:32:06 -0800 Subject: [PATCH 17/17] Nits that came up during review --- packages/react-dom/src/server/ReactPartialRenderer.js | 8 +++++--- packages/react-reconciler/src/ReactFiberBeginWork.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 3a2e971fe01bd..b07a9b160d7d2 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -630,7 +630,7 @@ class ReactDOMServerRenderer { previousWasTextNode: boolean; makeStaticMarkup: boolean; - providerStack: Array>; + providerStack: Array>; providerIndex: number; constructor(children: mixed, makeStaticMarkup: boolean) { @@ -675,14 +675,16 @@ class ReactDOMServerRenderer { 'Unexpected pop.', ); } - // $FlowFixMe - Intentionally unsound this.providerStack[this.providerIndex] = null; this.providerIndex -= 1; const context: ReactContext = provider.type.context; if (this.providerIndex < 0) { context.currentValue = context.defaultValue; } else { - const previousProvider = this.providerStack[this.providerIndex]; + // We assume this type is correct because of the index check above. + const previousProvider: ReactProvider = (this.providerStack[ + this.providerIndex + ]: any); context.currentValue = previousProvider.props.value; } } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 1dd9f7c0da8ed..ab616e4d3434d 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -767,8 +767,8 @@ export default function( // 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) { - pushProvider(workInProgress); workInProgress.stateNode = 0; + pushProvider(workInProgress); return bailoutOnAlreadyFinishedWork(current, workInProgress); } workInProgress.memoizedProps = newProps;