diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 69af9328bb8e7..4fd019ba0a978 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -8,7 +8,13 @@ */ import type {ReactElement} from 'shared/ReactElementType'; -import type {ReactCall, ReactPortal, ReactReturn} from 'shared/ReactTypes'; +import type { + ReactCall, + ReactPortal, + ReactReturn, + ReactProvider, + ReactConsumer, +} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime'; @@ -20,6 +26,8 @@ import { REACT_CALL_TYPE, REACT_RETURN_TYPE, REACT_PORTAL_TYPE, + REACT_PROVIDER_TYPE, + REACT_CONSUMER_TYPE, } from 'shared/ReactSymbols'; import { FunctionalComponent, @@ -29,6 +37,8 @@ import { CallComponent, ReturnComponent, Fragment, + ProviderComponent, + ConsumerComponent, } from 'shared/ReactTypeOfWork'; import emptyObject from 'fbjs/lib/emptyObject'; import invariant from 'fbjs/lib/invariant'; @@ -42,6 +52,8 @@ import { createFiberFromCall, createFiberFromReturn, createFiberFromPortal, + createFiberFromProvider, + createFiberFromConsumer, } from './ReactFiber'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; @@ -465,6 +477,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, @@ -537,6 +595,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)) { @@ -647,6 +723,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)) { @@ -755,6 +855,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)) { @@ -1362,6 +1486,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. @@ -1430,6 +1626,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 bda4d89639d90..bb9ede2a45fdb 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -12,6 +12,8 @@ import type { ReactFragment, ReactPortal, ReactReturn, + ReactProvider, + ReactConsumer, } from 'shared/ReactTypes'; import type {TypeOfWork} from 'shared/ReactTypeOfWork'; import type {TypeOfInternalContext} from './ReactTypeOfInternalContext'; @@ -31,6 +33,8 @@ import { CallComponent, ReturnComponent, Fragment, + ProviderComponent, + ConsumerComponent, } from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; @@ -442,3 +446,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 124f4fb7496ec..364463ebea7ff 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -8,7 +8,12 @@ */ import type {HostConfig} from 'react-reconciler'; -import type {ReactCall} from 'shared/ReactTypes'; +import type { + ReactCall, + 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 { CallHandlerPhase, ReturnComponent, Fragment, + 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'; @@ -145,13 +152,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); @@ -171,7 +175,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 { @@ -210,7 +214,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) { @@ -388,7 +392,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) { @@ -479,7 +483,8 @@ export default function( if ( typeof value === 'object' && value !== null && - typeof value.render === 'function' + typeof value.render === 'function' && + value.$$typeof === undefined ) { // Proceed under the assumption that this is a class instance workInProgress.tag = ClassComponent; @@ -487,7 +492,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); @@ -536,7 +541,7 @@ export default function( function updateCallComponent(current, workInProgress, renderExpirationTime) { let nextCall = (workInProgress.pendingProps: ReactCall); - 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 === nextCall) { @@ -579,7 +584,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) { @@ -606,6 +611,180 @@ 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, and + // their alternates. + let node = fiber; + while (node !== null) { + let canBreak = true; + if ( + node.expirationTime === NoWork || + node.expirationTime > renderExpirationTime + ) { + canBreak = false; + node.expirationTime = renderExpirationTime; + } + const alternate = node.alternate; + if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > renderExpirationTime) + ) { + canBreak = false; + alternate.expirationTime = renderExpirationTime; + } + if (canBreak) { + // Rest of the ancestor path already has sufficient priority. + break; + } + node = node.return; + } + // Don't scan deeper than a matching consumer. When we render the + // consumer, we'll continue scanning from that point. This way the + // scanning work is time-sliced. + nextFiber = null; + } else { + nextFiber = fiber.child; + } + 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) { + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + return; + } + 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; @@ -659,7 +838,7 @@ export default function( pushHostRootContext(workInProgress); break; case ClassComponent: - pushContextProvider(workInProgress); + pushLegacyContextProvider(workInProgress); break; case HostPortal: pushHostContainer( @@ -667,6 +846,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? @@ -743,6 +925,18 @@ export default function( ); case Fragment: return updateFragment(current, workInProgress); + case ProviderComponent: + return updateProviderComponent( + current, + workInProgress, + renderExpirationTime, + ); + case ConsumerComponent: + return updateConsumerComponent( + current, + workInProgress, + renderExpirationTime, + ); default: invariant( false, @@ -760,7 +954,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 9e07f34b348a6..4a5830df19a8e 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -31,6 +31,8 @@ import { CallComponent, CallHandlerPhase, ReturnComponent, + ProviderComponent, + ConsumerComponent, Fragment, } from 'shared/ReactTypeOfWork'; import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; @@ -575,6 +577,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 61a59f83c530b..e6302bb7a984d 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, @@ -40,6 +41,9 @@ const React = { Fragment: REACT_FRAGMENT_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 d914c0f25e270..72dd1f4351904 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -24,6 +24,12 @@ export const REACT_PORTAL_TYPE = hasSymbol export const REACT_FRAGMENT_TYPE = hasSymbol ? Symbol.for('react.fragment') : 0xeacb; +export const REACT_PROVIDER_TYPE = hasSymbol + ? Symbol.for('react.provider') + : 0xeacc; +export const REACT_CONSUMER_TYPE = hasSymbol + ? Symbol.for('react.consumer') + : 0xeacd; 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 ca5da127c7c19..ab12cfc1ae6da 100644 --- a/packages/shared/ReactTypeOfWork.js +++ b/packages/shared/ReactTypeOfWork.js @@ -7,7 +7,7 @@ * @flow */ -export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; +export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; export const IndeterminateComponent = 0; // Before we know whether it is functional or class export const FunctionalComponent = 1; @@ -20,3 +20,5 @@ export const CallComponent = 7; export const CallHandlerPhase = 8; export const ReturnComponent = 9; export const Fragment = 10; +export const ProviderComponent = 11; +export const ConsumerComponent = 12; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 900582e5d5ac8..416e1df7853c4 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; @@ -46,3 +48,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 +};