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-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js similarity index 85% rename from packages/react-dom/src/__tests__/ReactServerRendering-test.js rename to packages/react-dom/src/__tests__/ReactServerRendering-test.internal.js index f37fa2c15cd6e..ad351544f1253 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'); @@ -384,6 +387,99 @@ describe('ReactDOMServer', () => { expect(markup).toContain('hello, world'); }); + it('renders with new context API', () => { + 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; + }); + } + + 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 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 339411596bc1a..b07a9b160d7d2 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_CONTEXT_TYPE, } from 'shared/ReactSymbols'; import { @@ -192,7 +199,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 +610,7 @@ function resolve( } type Frame = { + type: mixed, domNamespace: string, children: FlatReactChildren, childIndex: number, @@ -622,12 +630,16 @@ class ReactDOMServerRenderer { previousWasTextNode: boolean; makeStaticMarkup: boolean; + providerStack: Array>; + providerIndex: number; + constructor(children: mixed, makeStaticMarkup: boolean) { const flatChildren = flattenTopLevelChildren(children); 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, @@ -642,6 +654,39 @@ 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.currentValue = provider.props.value; + } + + popProvider(provider: ReactProvider): void { + if (__DEV__) { + warning( + this.providerIndex > -1 && + provider === this.providerStack[this.providerIndex], + 'Unexpected pop.', + ); + } + this.providerStack[this.providerIndex] = null; + this.providerIndex -= 1; + const context: ReactContext = provider.type.context; + if (this.providerIndex < 0) { + context.currentValue = context.defaultValue; + } else { + // 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; + } } read(bytes: number): string | null { @@ -663,8 +708,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); + this.popProvider(provider); } continue; } @@ -723,6 +775,7 @@ class ReactDOMServerRenderer { } const nextChildren = toArray(nextChild); const frame: Frame = { + type: null, domNamespace: parentNamespace, children: nextChildren, childIndex: 0, @@ -738,12 +791,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 +814,7 @@ class ReactDOMServerRenderer { } this.stack.push(frame); return ''; + } case REACT_CALL_TYPE: case REACT_RETURN_TYPE: invariant( @@ -764,8 +824,62 @@ 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: any); + 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 = []; + } + + this.pushProvider(provider); + + this.stack.push(frame); + return ''; + } + case REACT_CONTEXT_TYPE: { + const consumer: ReactConsumer = (nextChild: any); + const nextProps = consumer.props; + const nextValue = consumer.type.currentValue; + + 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 +1166,7 @@ class ReactDOMServerRenderer { } const frame = { domNamespace: getChildNamespace(parentNamespace, element.type), - tag, + type: tag, children, childIndex: 0, context: context, diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index d36433099c982..fb6ba80555f2a 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -7,7 +7,7 @@ */ import type {ReactElement, Source} from 'shared/ReactElementType'; -import type {ReactFragment, ReactPortal} 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'; @@ -27,6 +27,8 @@ import { ReturnComponent, Fragment, Mode, + ContextProvider, + ContextConsumer, } from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; @@ -41,6 +43,8 @@ import { REACT_RETURN_TYPE, REACT_CALL_TYPE, REACT_STRICT_MODE_TYPE, + REACT_PROVIDER_TYPE, + REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -316,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: @@ -340,90 +337,92 @@ 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 && - 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: + fiberTag = ContextProvider; + break; + case REACT_CONTEXT_TYPE: + // This is a consumer + fiberTag = ContextConsumer; + 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; + fiber.expirationTime = expirationTime; + return fiber; + } 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); } } } } + 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; } +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, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index d0569a6b4ad90..ab616e4d3434d 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -8,12 +8,14 @@ */ import type {HostConfig} from 'react-reconciler'; +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'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import {enableNewContextAPI} from 'shared/ReactFeatureFlags'; import { IndeterminateComponent, FunctionalComponent, @@ -27,6 +29,8 @@ import { ReturnComponent, Fragment, Mode, + ContextProvider, + ContextConsumer, } from 'shared/ReactTypeOfWork'; import { PerformedWork, @@ -53,13 +57,15 @@ import {processUpdateQueue} from './ReactFiberUpdateQueue'; import { getMaskedContext, getUnmaskedContext, - hasContextChanged, - pushContextProvider, + hasContextChanged as hasLegacyContextChanged, + pushContextProvider as pushLegacyContextProvider, pushTopLevelContextObject, invalidateContextProvider, } from './ReactFiberContext'; +import {pushProvider} from './ReactFiberNewContext'; import {NoWork, Never} from './ReactFiberExpirationTime'; import {AsyncUpdates} from './ReactTypeOfInternalContext'; +import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt'; let didWarnAboutBadClass; let didWarnAboutGetDerivedStateOnFunctionalComponent; @@ -147,13 +153,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 +166,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 +192,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 +231,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 +400,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 +495,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 +525,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 +591,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 +634,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 +661,215 @@ export default function( return workInProgress.child; } + function propagateContextChange( + workInProgress: Fiber, + context: ReactContext, + changedBits: number, + renderExpirationTime: ExpirationTime, + ): void { + if (enableNewContextAPI) { + let fiber = workInProgress.child; + while (fiber !== null) { + let nextFiber; + // Visit this fiber. + switch (fiber.tag) { + case ContextConsumer: + // Check if the context matches. + const observedBits: number = fiber.stateNode | 0; + if (fiber.type === context && (observedBits & changedBits) !== 0) { + // Update the expiration time of all the ancestors, including + // the alternates. + let node = fiber; + while (node !== null) { + const alternate = node.alternate; + if ( + node.expirationTime === NoWork || + node.expirationTime > renderExpirationTime + ) { + node.expirationTime = renderExpirationTime; + if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > renderExpirationTime) + ) { + alternate.expirationTime = renderExpirationTime; + } + } else if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > renderExpirationTime) + ) { + alternate.expirationTime = renderExpirationTime; + } else { + // Neither alternate was updated, which means the rest of the + // ancestor path already has sufficient priority. + break; + } + node = node.return; + } + // Don't scan deeper than a matching consumer. When we render the + // consumer, we'll continue scanning from that point. This way the + // scanning work is time-sliced. + nextFiber = null; + } else { + // Traverse down. + nextFiber = fiber.child; + } + break; + case ContextProvider: + // Don't scan deeper if this is a matching provider + nextFiber = fiber.type === workInProgress.type ? null : fiber.child; + break; + default: + // Traverse down. + nextFiber = fiber.child; + break; + } + if (nextFiber !== null) { + // Set the return pointer of the child to the work-in-progress fiber. + nextFiber.return = fiber; + } else { + // No child. Traverse to next sibling. + nextFiber = fiber; + while (nextFiber !== null) { + if (nextFiber === workInProgress) { + // We're back to the root of this subtree. Exit. + nextFiber = null; + break; + } + let sibling = nextFiber.sibling; + if (sibling !== null) { + nextFiber = sibling; + break; + } + // No more siblings. Traverse up. + nextFiber = nextFiber.return; + } + } + fiber = nextFiber; + } + } + } + + function updateContextProvider( + current, + workInProgress, + renderExpirationTime, + ) { + if (enableNewContextAPI) { + const providerType: ReactProviderType = workInProgress.type; + const context: ReactContext = providerType.context; + + const newProps = workInProgress.pendingProps; + const oldProps = workInProgress.memoizedProps; + + 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) { + workInProgress.stateNode = 0; + pushProvider(workInProgress); + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + workInProgress.memoizedProps = newProps; + + const newValue = newProps.value; + + let changedBits: number; + if (oldProps === null) { + // Initial render + changedBits = MAX_SIGNED_31_BIT_INT; + } else { + const oldValue = oldProps.value; + // Use Object.is to compare the new context value to the old value. + // 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) + : 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, + context, + changedBits, + renderExpirationTime, + ); + } + } + } + + workInProgress.stateNode = changedBits; + pushProvider(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; + } + } + + function updateContextConsumer( + current, + workInProgress, + renderExpirationTime, + ) { + if (enableNewContextAPI) { + const context: ReactContext = workInProgress.type; + const newProps = workInProgress.pendingProps; + + const newValue = context.currentValue; + const changedBits = context.changedBits; + + if (changedBits !== 0) { + // Context change propagation stops at matching consumers, for time- + // slicing. Continue the propagation here. + propagateContextChange( + workInProgress, + context, + changedBits, + renderExpirationTime, + ); + } + + // 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 + observedBits = MAX_SIGNED_31_BIT_INT; + } + workInProgress.stateNode = observedBits; + + const newChildren = newProps.render(newValue); + reconcileChildren(current, workInProgress, newChildren); + return workInProgress.child; + } else { + return null; + } + } + /* function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) { let child = firstChild; @@ -710,7 +923,7 @@ export default function( pushHostRootContext(workInProgress); break; case ClassComponent: - pushContextProvider(workInProgress); + pushLegacyContextProvider(workInProgress); break; case HostPortal: pushHostContainer( @@ -718,6 +931,9 @@ export default function( workInProgress.stateNode.containerInfo, ); break; + case ContextProvider: + pushProvider(workInProgress); + break; } // TODO: What if this is currently in progress? // How can that happen? How is this not being cloned? @@ -796,6 +1012,18 @@ export default function( return updateFragment(current, workInProgress); case Mode: return updateMode(current, workInProgress); + case ContextProvider: + return updateContextProvider( + current, + workInProgress, + renderExpirationTime, + ); + case ContextConsumer: + return updateContextConsumer( + current, + workInProgress, + renderExpirationTime, + ); default: invariant( false, @@ -813,7 +1041,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..3cf970b844137 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -30,6 +30,8 @@ import { CallComponent, CallHandlerPhase, ReturnComponent, + ContextProvider, + ContextConsumer, Fragment, Mode, } from 'shared/ReactTypeOfWork'; @@ -38,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, @@ -401,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; @@ -583,6 +586,12 @@ export default function( popHostContainer(workInProgress); updateHostContainer(workInProgress); return null; + case ContextProvider: + // Pop provider fiber + popProvider(workInProgress); + return null; + case ContextConsumer: + return null; // Error cases case IndeterminateComponent: invariant( diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.js b/packages/react-reconciler/src/ReactFiberExpirationTime.js index 182740f89584e..dca42c4e3db2f 100644 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.js +++ b/packages/react-reconciler/src/ReactFiberExpirationTime.js @@ -7,12 +7,14 @@ * @flow */ +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 = 2147483647; // Max int32: Math.pow(2, 31) - 1 +export const Never = MAX_SIGNED_31_BIT_INT; const UNIT_SIZE = 10; const MAGIC_NUMBER_OFFSET = 2; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js new file mode 100644 index 0000000000000..ba59ae5998b94 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -0,0 +1,70 @@ +/** + * 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; + +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 { + if (__DEV__) { + warning(index > -1 && providerFiber === stack[index], 'Unexpected pop.'); + } + stack[index] = null; + index -= 1; + const context: ReactContext = providerFiber.type.context; + if (index < 0) { + context.currentValue = context.defaultValue; + context.changedBits = 0; + } else { + const previousProviderFiber = stack[index]; + context.currentValue = previousProviderFiber.pendingProps.value; + context.changedBits = previousProviderFiber.stateNode; + } +} + +export function resetProviderStack(): void { + for (let i = index; i > -1; i--) { + const providerFiber = stack[i]; + const context: ReactContext = providerFiber.type.context; + context.currentValue = context.defaultValue; + context.changedBits = 0; + stack[i] = null; + if (__DEV__) { + context._currentRenderer = null; + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index a2d0a5d354d59..e223f2aa26fd6 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -33,6 +33,7 @@ import { HostComponent, HostPortal, ClassComponent, + ContextProvider, } 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 ContextProvider: + popProvider(node); + break; } if (node === to || node.alternate === to) { stopFailedWorkTimer(node); diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js new file mode 100644 index 0000000000000..cf366cf05a43b --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -0,0 +1,784 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); +ReactFeatureFlags.enableNewContextAPI = true; + +let React = require('react'); +let ReactNoop; +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'); + }); + + // 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.unstable_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.unstable_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.unstable_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.unstable_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.unstable_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'), + ]); + }); + + it('compares context values with Object.is semantics', () => { + const Context = React.unstable_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')]); + }); + + it('context unwinds when interrupted', () => { + const Context = React.unstable_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'), + ]); + }); + + 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')]); + }); + + 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', + ); + } + }); + + 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 Fragment = React.Fragment; + const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; + + const FLUSH_ALL = 'FLUSH_ALL'; + function flushAll() { + return { + type: FLUSH_ALL, + toString() { + return `flushAll()`; + }, + }; + } + + const FLUSH = 'FLUSH'; + function flush(unitsOfWork) { + return { + type: FLUSH, + unitsOfWork, + toString() { + return `flush(${unitsOfWork})`; + }, + }; + } + + const UPDATE = 'UPDATE'; + function update(key, value) { + return { + type: UPDATE, + key, + value, + toString() { + return `update('${key}', ${value})`; + }, + }; + } + + function randomInteger(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; + } + + function randomAction() { + switch (randomInteger(0, 3)) { + case 0: + return flushAll(); + case 1: + return flush(randomInteger(0, 500)); + case 2: + const key = contextKeys[randomInteger(0, contextKeys.length)]; + const value = randomInteger(1, 10); + return update(key, value); + default: + throw new Error('Switch statement should be exhaustive'); + } + } + + function randomActions(n) { + let actions = []; + for (let i = 0; i < n; i++) { + actions.push(randomAction()); + } + return actions; + } + + function ContextSimulator(maxDepth) { + const contexts = new Map( + contextKeys.map(key => { + const Context = React.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; + } + } + + 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 simulate(seed, actions) { + const rand = gen.create(seed); + let finalExpectedValues = initialValues; + function updateRoot() { + ReactNoop.render( + , + ); + } + updateRoot(); + + actions.forEach(action => { + switch (action.type) { + case FLUSH_ALL: + ReactNoop.flush(); + break; + case FLUSH: + ReactNoop.flushUnitsOfWork(action.unitsOfWork); + break; + case UPDATE: + finalExpectedValues = { + ...finalExpectedValues, + [action.key]: action.value, + }; + updateRoot(); + break; + default: + throw new Error('Switch statement should be exhaustive'); + } + assertConsistentTree(); + }); + + ReactNoop.flush(); + assertConsistentTree(finalExpectedValues); + } + + return {simulate}; + } + + it('hard-coded tests', () => { + const {simulate} = ContextSimulator(5); + simulate('randomSeed', [flush(3), update('A', 4)]); + }); + + it('generated tests', () => { + const {simulate} = ContextSimulator(5); + + const LIMIT = 100; + for (let i = 0; i < LIMIT; i++) { + const seed = Math.random() + .toString(36) + .substr(2, 5); + const actions = randomActions(5); + try { + simulate(seed, actions); + } catch (error) { + console.error(` +Context fuzz tester error! Copy and paste the following line into the test suite: + simulate('${seed}', ${actions.join(', ')}); +`); + throw error; + } + } + }); + }); +}); diff --git a/packages/react-reconciler/src/maxSigned31BitInt.js b/packages/react-reconciler/src/maxSigned31BitInt.js new file mode 100644 index 0000000000000..2b6e167d0dfe7 --- /dev/null +++ b/packages/react-reconciler/src/maxSigned31BitInt.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 + */ + +// 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/react/src/React.js b/packages/react/src/React.js index 81247eba6f0d4..110dd0a9c8264 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -18,12 +18,14 @@ import { cloneElement, isValidElement, } from './ReactElement'; +import {createContext} from 'shared/ReactContext'; import { createElementWithValidation, createFactoryWithValidation, cloneElementWithValidation, } from './ReactElementValidator'; import ReactDebugCurrentFrame from './ReactDebugCurrentFrame'; +import {enableNewContextAPI} from 'shared/ReactFeatureFlags'; const React = { Children: { @@ -55,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/ReactContext.js b/packages/shared/ReactContext.js new file mode 100644 index 0000000000000..2cc5bd6c43688 --- /dev/null +++ b/packages/shared/ReactContext.js @@ -0,0 +1,91 @@ +/** + * 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_CONTEXT_TYPE, + REACT_ELEMENT_TYPE, +} from 'shared/ReactSymbols'; + +import type { + ReactContext, + ReactConsumer, + ReactProvider, + ReactNodeList, +} from 'shared/ReactTypes'; + +import warning from 'fbjs/lib/warning'; + +export function createContext( + defaultValue: T, + calculateChangedBits: ?(a: T, b: T) => number, +): ReactContext { + let providerType; + + 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: ReactContext = { + $$typeof: REACT_CONTEXT_TYPE, + provide(value: T, children: ReactNodeList, key?: string): ReactProvider { + return { + $$typeof: REACT_ELEMENT_TYPE, + type: providerType, + key: key === null || key === undefined ? null : '' + key, + ref: null, + props: { + value, + children, + }, + }; + }, + consume( + render: (value: T) => ReactNodeList, + observedBits?: number, + key?: string, + ): ReactConsumer { + return { + $$typeof: REACT_ELEMENT_TYPE, + type: context, + key: key === null || key === undefined ? null : '' + key, + ref: null, + props: { + observedBits, + render, + }, + }; + }, + calculateChangedBits, + defaultValue, + currentValue: defaultValue, + changedBits: 0, + }; + + providerType = { + $$typeof: REACT_PROVIDER_TYPE, + context, + }; + + if (__DEV__) { + context._currentRenderer = null; + } + + return context; +} 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/ReactSymbols.js b/packages/shared/ReactSymbols.js index de205bd2d244e..037d4a38d0f39 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_CONTEXT_TYPE = hasSymbol + ? Symbol.for('react.context') + : 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..3d1bfc3f37584 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 ContextConsumer = 12; +export const ContextProvider = 13; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index c68864323d7cb..0e29678f35b00 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; @@ -47,6 +49,50 @@ export type ReactReturn = { }, }; +export type ReactProvider = { + $$typeof: Symbol | number, + type: ReactProviderType, + key: null | string, + ref: null, + props: { + value: T, + children?: ReactNodeList, + }, +}; + +export type ReactProviderType = { + $$typeof: Symbol | number, + context: ReactContext, +}; + +export type ReactConsumer = { + $$typeof: Symbol | number, + type: ReactContext, + key: null | string, + ref: null, + props: { + render: (value: T) => ReactNodeList, + bits?: number, + }, +}; + +export type ReactContext = { + $$typeof: Symbol | number, + provide(value: T, children: ReactNodeList, key?: string): ReactProvider, + consume( + render: (value: T) => ReactNodeList, + observedBits?: number, + key?: string, + ): ReactConsumer, + calculateChangedBits: ((a: T, b: T) => number) | null, + defaultValue: T, + currentValue: T, + changedBits: number, + + // DEV only + _currentRenderer?: Object | null, +}; + export type ReactPortal = { $$typeof: Symbol | number, key: null | string, 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; 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"