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"