diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 6d92d8a156b2b..aee2b0a23318a 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -237,19 +237,25 @@ function markRef(current: Fiber | null, workInProgress: Fiber) {
}
}
-function updateFunctionalComponent(current, workInProgress) {
+function updateFunctionalComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+) {
const fn = workInProgress.type;
const nextProps = workInProgress.pendingProps;
+ const hasPendingContext = prepareToReadContext(
+ workInProgress,
+ renderExpirationTime,
+ );
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) {
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
- }
+ } else if (workInProgress.memoizedProps === nextProps && !hasPendingContext) {
// TODO: consider bringing fn.shouldComponentUpdate() back.
// It used to be here.
+ return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
const unmaskedContext = getUnmaskedContext(workInProgress);
@@ -265,6 +271,7 @@ function updateFunctionalComponent(current, workInProgress) {
} else {
nextChildren = fn(nextProps, context);
}
+
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren);
@@ -281,6 +288,11 @@ function updateClassComponent(
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
const hasContext = pushLegacyContextProvider(workInProgress);
+ const hasPendingNewContext = prepareToReadContext(
+ workInProgress,
+ renderExpirationTime,
+ );
+
let shouldUpdate;
if (current === null) {
if (workInProgress.stateNode === null) {
@@ -297,6 +309,7 @@ function updateClassComponent(
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
+ hasPendingNewContext,
renderExpirationTime,
);
}
@@ -304,6 +317,7 @@ function updateClassComponent(
shouldUpdate = updateClassInstance(
current,
workInProgress,
+ hasPendingNewContext,
renderExpirationTime,
);
}
@@ -580,6 +594,8 @@ function mountIndeterminateComponent(
const unmaskedContext = getUnmaskedContext(workInProgress);
const context = getMaskedContext(workInProgress, unmaskedContext);
+ prepareToReadContext(workInProgress, renderExpirationTime);
+
let value;
if (__DEV__) {
@@ -1082,7 +1098,11 @@ function beginWork(
renderExpirationTime,
);
case FunctionalComponent:
- return updateFunctionalComponent(current, workInProgress);
+ return updateFunctionalComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case ClassComponent:
return updateClassComponent(
current,
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js
index a0ae9fb7823f9..ddbb5524a9921 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.js
@@ -231,7 +231,7 @@ function checkShouldComponentUpdate(
newProps,
oldState,
newState,
- newContext,
+ nextLegacyContext,
) {
const instance = workInProgress.stateNode;
const ctor = workInProgress.type;
@@ -240,7 +240,7 @@ function checkShouldComponentUpdate(
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
- newContext,
+ nextLegacyContext,
);
stopPhaseTimer();
@@ -616,15 +616,15 @@ function callComponentWillReceiveProps(
workInProgress,
instance,
newProps,
- newContext,
+ nextLegacyContext,
) {
const oldState = instance.state;
startPhaseTimer(workInProgress, 'componentWillReceiveProps');
if (typeof instance.componentWillReceiveProps === 'function') {
- instance.componentWillReceiveProps(newProps, newContext);
+ instance.componentWillReceiveProps(newProps, nextLegacyContext);
}
if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') {
- instance.UNSAFE_componentWillReceiveProps(newProps, newContext);
+ instance.UNSAFE_componentWillReceiveProps(newProps, nextLegacyContext);
}
stopPhaseTimer();
@@ -736,6 +736,7 @@ function mountClassInstance(
function resumeMountClassInstance(
workInProgress: Fiber,
+ hasPendingNewContext: boolean,
renderExpirationTime: ExpirationTime,
): boolean {
const ctor = workInProgress.type;
@@ -746,8 +747,11 @@ function resumeMountClassInstance(
instance.props = oldProps;
const oldContext = instance.context;
- const newUnmaskedContext = getUnmaskedContext(workInProgress);
- const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
+ const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress);
+ const nextLegacyContext = getMaskedContext(
+ workInProgress,
+ nextLegacyUnmaskedContext,
+ );
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
@@ -765,12 +769,12 @@ function resumeMountClassInstance(
(typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
typeof instance.componentWillReceiveProps === 'function')
) {
- if (oldProps !== newProps || oldContext !== newContext) {
+ if (oldProps !== newProps || oldContext !== nextLegacyContext) {
callComponentWillReceiveProps(
workInProgress,
instance,
newProps,
- newContext,
+ nextLegacyContext,
);
}
}
@@ -794,6 +798,7 @@ function resumeMountClassInstance(
oldProps === newProps &&
oldState === newState &&
!hasContextChanged() &&
+ !hasPendingNewContext &&
!checkHasForceUpdateAfterProcessing()
) {
// If an update was already in progress, we should schedule an Update
@@ -815,13 +820,14 @@ function resumeMountClassInstance(
const shouldUpdate =
checkHasForceUpdateAfterProcessing() ||
+ hasPendingNewContext ||
checkShouldComponentUpdate(
workInProgress,
oldProps,
newProps,
oldState,
newState,
- newContext,
+ nextLegacyContext,
);
if (shouldUpdate) {
@@ -861,7 +867,7 @@ function resumeMountClassInstance(
// if shouldComponentUpdate returns false.
instance.props = newProps;
instance.state = newState;
- instance.context = newContext;
+ instance.context = nextLegacyContext;
return shouldUpdate;
}
@@ -870,6 +876,7 @@ function resumeMountClassInstance(
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
+ hasPendingNewContext: boolean,
renderExpirationTime: ExpirationTime,
): boolean {
const ctor = workInProgress.type;
@@ -880,8 +887,11 @@ function updateClassInstance(
instance.props = oldProps;
const oldContext = instance.context;
- const newUnmaskedContext = getUnmaskedContext(workInProgress);
- const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
+ const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress);
+ const nextLegacyContext = getMaskedContext(
+ workInProgress,
+ nextLegacyUnmaskedContext,
+ );
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
@@ -899,12 +909,12 @@ function updateClassInstance(
(typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
typeof instance.componentWillReceiveProps === 'function')
) {
- if (oldProps !== newProps || oldContext !== newContext) {
+ if (oldProps !== newProps || oldContext !== nextLegacyContext) {
callComponentWillReceiveProps(
workInProgress,
instance,
newProps,
- newContext,
+ nextLegacyContext,
);
}
}
@@ -929,6 +939,7 @@ function updateClassInstance(
oldProps === newProps &&
oldState === newState &&
!hasContextChanged() &&
+ !hasPendingNewContext &&
!checkHasForceUpdateAfterProcessing()
) {
// If an update was already in progress, we should schedule an Update
@@ -963,13 +974,14 @@ function updateClassInstance(
const shouldUpdate =
checkHasForceUpdateAfterProcessing() ||
+ hasPendingNewContext ||
checkShouldComponentUpdate(
workInProgress,
oldProps,
newProps,
oldState,
newState,
- newContext,
+ nextLegacyContext,
);
if (shouldUpdate) {
@@ -982,10 +994,14 @@ function updateClassInstance(
) {
startPhaseTimer(workInProgress, 'componentWillUpdate');
if (typeof instance.componentWillUpdate === 'function') {
- instance.componentWillUpdate(newProps, newState, newContext);
+ instance.componentWillUpdate(newProps, newState, nextLegacyContext);
}
if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
- instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
+ instance.UNSAFE_componentWillUpdate(
+ newProps,
+ newState,
+ nextLegacyContext,
+ );
}
stopPhaseTimer();
}
@@ -1025,7 +1041,7 @@ function updateClassInstance(
// if shouldComponentUpdate returns false.
instance.props = newProps;
instance.state = newState;
- instance.context = newContext;
+ instance.context = nextLegacyContext;
return shouldUpdate;
}
diff --git a/packages/react-reconciler/src/ReactFiberDispatcher.js b/packages/react-reconciler/src/ReactFiberDispatcher.js
new file mode 100644
index 0000000000000..f515457770168
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberDispatcher.js
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import {readContext} from './ReactFiberNewContext';
+
+export const Dispatcher = {
+ readContext,
+};
diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js
index 2f10fb537f16d..54f924ad2f67d 100644
--- a/packages/react-reconciler/src/ReactFiberScheduler.js
+++ b/packages/react-reconciler/src/ReactFiberScheduler.js
@@ -146,6 +146,7 @@ import {
commitAttachRef,
commitDetachRef,
} from './ReactFiberCommitWork';
+import {Dispatcher} from './ReactFiberDispatcher';
export type Deadline = {
timeRemaining: () => number,
@@ -1011,6 +1012,7 @@ function renderRoot(
'by a bug in React. Please file an issue.',
);
isWorking = true;
+ ReactCurrentOwner.currentDispatcher = Dispatcher;
const expirationTime = root.nextExpirationTimeToWorkOn;
@@ -1101,6 +1103,7 @@ function renderRoot(
// We're done performing work. Time to clean up.
isWorking = false;
+ ReactCurrentOwner.currentDispatcher = null;
// Yield back to main thread.
if (didFatal) {
diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
index 95355e2a1864d..e090239540bd6 100644
--- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
@@ -30,1105 +30,1300 @@ describe('ReactNewContext', () => {
// return {type: 'div', children, prop: undefined};
// }
+ function Text(props) {
+ ReactNoop.yield(props.text);
+ return ;
+ }
+
function span(prop) {
return {type: 'span', children: [], prop};
}
- it('simple mount and update', () => {
- const Context = React.createContext(1);
-
- function Consumer(props) {
- return (
-
- {value => }
-
- );
- }
-
- const Indirection = React.Fragment;
-
- function App(props) {
- return (
-
-
-
-
-
-
-
- );
- }
-
- ReactNoop.render();
- ReactNoop.flush();
- expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
-
- // Update
- ReactNoop.render();
- ReactNoop.flush();
- expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
- });
+ // We have several ways of reading from context. sharedContextTests runs
+ // a suite of tests for a given context consumer implementation.
+ sharedContextTests('Context.Consumer', Context => Context.Consumer);
+ sharedContextTests(
+ 'Context.unstable_read inside functional component',
+ Context =>
+ function Consumer(props) {
+ const observedBits = props.unstable_observedBits;
+ const contextValue = Context.unstable_read(observedBits);
+ const render = props.children;
+ return render(contextValue);
+ },
+ );
+ sharedContextTests(
+ 'Context.unstable_read inside class component',
+ Context =>
+ class Consumer extends React.Component {
+ render() {
+ const observedBits = this.props.unstable_observedBits;
+ const contextValue = Context.unstable_read(observedBits);
+ const render = this.props.children;
+ return render(contextValue);
+ }
+ },
+ );
- it('propagates through shouldComponentUpdate false', () => {
- const Context = React.createContext(1);
+ function sharedContextTests(label, getConsumer) {
+ describe(`reading context with ${label}`, () => {
+ it('simple mount and update', () => {
+ const Context = React.createContext(1);
+ const Consumer = getConsumer(Context);
- function Provider(props) {
- ReactNoop.yield('Provider');
- return (
-
- {props.children}
-
- );
- }
+ const Indirection = React.Fragment;
- function Consumer(props) {
- ReactNoop.yield('Consumer');
- return (
-
- {value => {
- ReactNoop.yield('Consumer render prop');
- return ;
- }}
-
- );
- }
+ function App(props) {
+ return (
+
+
+
+
+ {value => }
+
+
+
+
+ );
+ }
- class Indirection extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
- render() {
- ReactNoop.yield('Indirection');
- return this.props.children;
- }
- }
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
- function App(props) {
- ReactNoop.yield('App');
- return (
-
-
-
-
-
-
-
- );
- }
+ // Update
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
+ });
- 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('propagates through shouldComponentUpdate false', () => {
+ const Context = React.createContext(1);
+ const ContextConsumer = getConsumer(Context);
- it('consumers bail out if context value is the same', () => {
- const Context = React.createContext(1);
+ function Provider(props) {
+ ReactNoop.yield('Provider');
+ return (
+
+ {props.children}
+
+ );
+ }
- function Provider(props) {
- ReactNoop.yield('Provider');
- return (
-
- {props.children}
-
- );
- }
+ function Consumer(props) {
+ ReactNoop.yield('Consumer');
+ return (
+
+ {value => {
+ ReactNoop.yield('Consumer render prop');
+ return ;
+ }}
+
+ );
+ }
- function Consumer(props) {
- ReactNoop.yield('Consumer');
- return (
-
- {value => {
- ReactNoop.yield('Consumer render prop');
- return ;
- }}
-
- );
- }
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ ReactNoop.yield('Indirection');
+ return this.props.children;
+ }
+ }
- class Indirection extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
- render() {
- ReactNoop.yield('Indirection');
- return this.props.children;
- }
- }
+ function App(props) {
+ ReactNoop.yield('App');
+ return (
+
+
+
+
+
+
+
+ );
+ }
- function App(props) {
- ReactNoop.yield('App');
- return (
-
-
-
-
-
-
-
- );
- }
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([
+ 'App',
+ 'Provider',
+ 'Indirection',
+ 'Indirection',
+ 'Consumer',
+ 'Consumer render prop',
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
+
+ // Update
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([
+ 'App',
+ 'Provider',
+ 'Consumer render prop',
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 3')]);
+ });
+
+ it('consumers bail out if context value is the same', () => {
+ const Context = React.createContext(1);
+ const ContextConsumer = getConsumer(Context);
+
+ function Provider(props) {
+ ReactNoop.yield('Provider');
+ return (
+
+ {props.children}
+
+ );
+ }
- 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')]);
- });
+ function Consumer(props) {
+ ReactNoop.yield('Consumer');
+ return (
+
+ {value => {
+ ReactNoop.yield('Consumer render prop');
+ return ;
+ }}
+
+ );
+ }
- it('nested providers', () => {
- const Context = React.createContext(1);
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ ReactNoop.yield('Indirection');
+ return this.props.children;
+ }
+ }
- function Provider(props) {
- return (
-
- {contextValue => (
- // Multiply previous context value by 2, unless prop overrides
-
- {props.children}
-
- )}
-
- );
- }
+ function App(props) {
+ ReactNoop.yield('App');
+ return (
+
+
+
+
+
+
+
+ );
+ }
- function Consumer(props) {
- return (
-
- {value => }
-
- );
- }
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([
+ 'App',
+ 'Provider',
+ 'Indirection',
+ 'Indirection',
+ 'Consumer',
+ 'Consumer render prop',
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
+
+ // Update with the same context value
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([
+ 'App',
+ 'Provider',
+ // Don't call render prop again
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 2')]);
+ });
+
+ it('nested providers', () => {
+ const Context = React.createContext(1);
+ const Consumer = getConsumer(Context);
+
+ function Provider(props) {
+ return (
+
+ {contextValue => (
+ // Multiply previous context value by 2, unless prop overrides
+
+ {props.children}
+
+ )}
+
+ );
+ }
- class Indirection extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
- render() {
- return this.props.children;
- }
- }
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ return this.props.children;
+ }
+ }
- function App(props) {
- return (
-
-
-
+ function App(props) {
+ return (
+
-
+
+
+
+ {value => }
+
+
+
-
-
- );
- }
+ );
+ }
- ReactNoop.render();
- ReactNoop.flush();
- expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]);
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 8')]);
- // Update
- ReactNoop.render();
- ReactNoop.flush();
- expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]);
- });
+ // Update
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([span('Result: 12')]);
+ });
- it('should provide the correct (default) values to consumers outside of a provider', () => {
- const FooContext = React.createContext({value: 'foo-initial'});
- const BarContext = React.createContext({value: 'bar-initial'});
-
- const Verify = ({actual, expected}) => {
- expect(expected).toBe(actual);
- return null;
- };
-
- ReactNoop.render(
-
-
-
- {({value}) => }
-
-
-
-
- {({value}) => }
-
-
-
-
-
- {({value}) => }
-
-
- {({value}) => }
-
- ,
- );
- ReactNoop.flush();
- });
+ it('should provide the correct (default) values to consumers outside of a provider', () => {
+ const FooContext = React.createContext({value: 'foo-initial'});
+ const BarContext = React.createContext({value: 'bar-initial'});
+ const FooConsumer = getConsumer(FooContext);
+ const BarConsumer = getConsumer(BarContext);
+
+ const Verify = ({actual, expected}) => {
+ expect(expected).toBe(actual);
+ return null;
+ };
+
+ ReactNoop.render(
+
+
+
+ {({value}) => }
+
+
+
+
+ {({value}) => (
+
+ )}
+
+
+
+
+
+ {({value}) => }
+
+
+ {({value}) => }
+
+ ,
+ );
+ ReactNoop.flush();
+ });
+
+ it('multiple consumers in different branches', () => {
+ const Context = React.createContext(1);
+ const Consumer = getConsumer(Context);
+
+ function Provider(props) {
+ return (
+
+ {contextValue => (
+ // Multiply previous context value by 2, unless prop overrides
+
+ {props.children}
+
+ )}
+
+ );
+ }
+
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ return this.props.children;
+ }
+ }
+
+ function App(props) {
+ return (
+
+
+
+
+
+ {value => }
+
+
+
+
+
+ {value => }
+
+
+
+
+ );
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Result: 4'),
+ span('Result: 2'),
+ ]);
- it('multiple consumers in different branches', () => {
- const Context = React.createContext(1);
+ // Update
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Result: 6'),
+ span('Result: 3'),
+ ]);
- function Provider(props) {
- return (
-
- {contextValue => (
- // Multiply previous context value by 2, unless prop overrides
-
+ // Another update
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Result: 8'),
+ span('Result: 4'),
+ ]);
+ });
+
+ it('compares context values with Object.is semantics', () => {
+ const Context = React.createContext(1);
+ const ContextConsumer = getConsumer(Context);
+
+ function Provider(props) {
+ ReactNoop.yield('Provider');
+ return (
+
{props.children}
- )}
-
- );
- }
-
- function Consumer(props) {
- return (
-
- {value => }
-
- );
- }
+ );
+ }
- class Indirection extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
- render() {
- return this.props.children;
- }
- }
+ function Consumer(props) {
+ ReactNoop.yield('Consumer');
+ return (
+
+ {value => {
+ ReactNoop.yield('Consumer render prop');
+ return ;
+ }}
+
+ );
+ }
- function App(props) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ ReactNoop.yield('Indirection');
+ return this.props.children;
+ }
+ }
- 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'),
- ]);
- });
+ function App(props) {
+ ReactNoop.yield('App');
+ return (
+
+
+
+
+
+
+
+ );
+ }
- it('compares context values with Object.is semantics', () => {
- const Context = React.createContext(1);
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([
+ 'App',
+ 'Provider',
+ 'Indirection',
+ 'Indirection',
+ 'Consumer',
+ 'Consumer render prop',
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
+
+ // Update
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([
+ 'App',
+ 'Provider',
+ // Consumer should not re-render again
+ // 'Consumer render prop',
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span('Result: NaN')]);
+ });
+
+ it('context unwinds when interrupted', () => {
+ const Context = React.createContext('Default');
+ const ContextConsumer = getConsumer(Context);
+
+ function Consumer(props) {
+ return (
+
+ {value => }
+
+ );
+ }
- function Provider(props) {
- ReactNoop.yield('Provider');
- return (
-
- {props.children}
-
- );
- }
+ function BadRender() {
+ throw new Error('Bad render');
+ }
- function Consumer(props) {
- ReactNoop.yield('Consumer');
- return (
-
- {value => {
- ReactNoop.yield('Consumer render prop');
- return ;
- }}
-
- );
- }
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ componentDidCatch(error) {
+ this.setState({error});
+ }
+ render() {
+ if (this.state.error) {
+ return null;
+ }
+ return this.props.children;
+ }
+ }
- class Indirection extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
- render() {
- ReactNoop.yield('Indirection');
- return this.props.children;
- }
- }
+ function App(props) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
- function App(props) {
- ReactNoop.yield('App');
- return (
-
-
-
-
-
-
-
- );
- }
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([
+ // The second provider should use the default value.
+ span('Result: Does not unwind'),
+ ]);
+ });
+
+ it('can skip consumers with bitmask', () => {
+ const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
+ let result = 0;
+ if (a.foo !== b.foo) {
+ result |= 0b01;
+ }
+ if (a.bar !== b.bar) {
+ result |= 0b10;
+ }
+ return result;
+ });
+ const Consumer = getConsumer(Context);
- 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')]);
- });
+ function Provider(props) {
+ return (
+
+ {props.children}
+
+ );
+ }
- it('context unwinds when interrupted', () => {
- const Context = React.createContext('Default');
+ function Foo() {
+ return (
+
+ {value => {
+ ReactNoop.yield('Foo');
+ return ;
+ }}
+
+ );
+ }
- function Consumer(props) {
- return (
-
- {value => }
-
- );
- }
+ function Bar() {
+ return (
+
+ {value => {
+ ReactNoop.yield('Bar');
+ return ;
+ }}
+
+ );
+ }
- function BadRender() {
- throw new Error('Bad render');
- }
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ return this.props.children;
+ }
+ }
- class ErrorBoundary extends React.Component {
- state = {error: null};
- componentDidCatch(error) {
- this.setState({error});
- }
- render() {
- if (this.state.error) {
- return null;
+ function App(props) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
}
- return this.props.children;
- }
- }
- function App(props) {
- return (
-
-
-
-
-
-
-
-
-
-
- );
- }
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 1'),
+ span('Bar: 1'),
+ ]);
+
+ // Update only foo
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 2'),
+ span('Bar: 1'),
+ ]);
+
+ // Update only bar
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Bar']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 2'),
+ span('Bar: 2'),
+ ]);
+
+ // Update both
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 3'),
+ span('Bar: 3'),
+ ]);
+ });
+
+ it('can skip parents with bitmask bailout while updating their children', () => {
+ const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
+ let result = 0;
+ if (a.foo !== b.foo) {
+ result |= 0b01;
+ }
+ if (a.bar !== b.bar) {
+ result |= 0b10;
+ }
+ return result;
+ });
+ const Consumer = getConsumer(Context);
- ReactNoop.render();
- ReactNoop.flush();
- expect(ReactNoop.getChildren()).toEqual([
- // The second provider should use the default value.
- span('Result: Does not unwind'),
- ]);
- });
+ function Provider(props) {
+ return (
+
+ {props.children}
+
+ );
+ }
- it('can skip consumers with bitmask', () => {
- const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
- let result = 0;
- if (a.foo !== b.foo) {
- result |= 0b01;
- }
- if (a.bar !== b.bar) {
- result |= 0b10;
- }
- return result;
- });
+ function Foo(props) {
+ return (
+
+ {value => {
+ ReactNoop.yield('Foo');
+ return (
+
+
+ {props.children && props.children()}
+
+ );
+ }}
+
+ );
+ }
- function Provider(props) {
- return (
-
- {props.children}
-
- );
- }
+ function Bar(props) {
+ return (
+
+ {value => {
+ ReactNoop.yield('Bar');
+ return (
+
+
+ {props.children && props.children()}
+
+ );
+ }}
+
+ );
+ }
- function Foo() {
- return (
-
- {value => {
- ReactNoop.yield('Foo');
- return ;
- }}
-
- );
- }
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ return this.props.children;
+ }
+ }
- function Bar() {
- return (
-
- {value => {
- ReactNoop.yield('Bar');
- return ;
- }}
-
- );
- }
+ function App(props) {
+ return (
+
+
+
+ {/* Use a render prop so we don't test constant elements. */}
+ {() => (
+
+
+ {() => (
+
+
+
+ )}
+
+
+ )}
+
+
+
+ );
+ }
- class Indirection extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
- render() {
- return this.props.children;
- }
- }
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 1'),
+ span('Bar: 1'),
+ span('Foo: 1'),
+ ]);
+
+ // Update only foo
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Foo']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 2'),
+ span('Bar: 1'),
+ span('Foo: 2'),
+ ]);
+
+ // Update only bar
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Bar']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 2'),
+ span('Bar: 2'),
+ span('Foo: 2'),
+ ]);
+
+ // Update both
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 3'),
+ span('Bar: 3'),
+ span('Foo: 3'),
+ ]);
+ });
+
+ it("does not re-render if there's an update in a child", () => {
+ const Context = React.createContext(0);
+ const Consumer = getConsumer(Context);
+
+ let child;
+ class Child extends React.Component {
+ state = {step: 0};
+ render() {
+ ReactNoop.yield('Child');
+ return (
+
+ );
+ }
+ }
- function App(props) {
- return (
-
-
-
-
-
-
-
-
-
-
- );
- }
+ function App(props) {
+ return (
+
+
+ {value => {
+ ReactNoop.yield('Consumer render prop');
+ return (child = inst)} context={value} />;
+ }}
+
+
+ );
+ }
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
- expect(ReactNoop.getChildren()).toEqual([span('Foo: 1'), span('Bar: 1')]);
+ // Initial mount
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']);
+ expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]);
- // Update only foo
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Foo']);
- expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 1')]);
+ child.setState({step: 1});
+ expect(ReactNoop.flush()).toEqual(['Child']);
+ expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]);
+ });
- // Update only bar
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Bar']);
- expect(ReactNoop.getChildren()).toEqual([span('Foo: 2'), span('Bar: 2')]);
+ it('consumer bails out if value is unchanged and something above bailed out', () => {
+ const Context = React.createContext(0);
+ const Consumer = getConsumer(Context);
- // Update both
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
- expect(ReactNoop.getChildren()).toEqual([span('Foo: 3'), span('Bar: 3')]);
- });
+ function renderChildValue(value) {
+ ReactNoop.yield('Consumer');
+ return ;
+ }
- it('can skip parents with bitmask bailout while updating their children', () => {
- const Context = React.createContext({foo: 0, bar: 0}, (a, b) => {
- let result = 0;
- if (a.foo !== b.foo) {
- result |= 0b01;
- }
- if (a.bar !== b.bar) {
- result |= 0b10;
- }
- return result;
- });
+ function ChildWithInlineRenderCallback() {
+ ReactNoop.yield('ChildWithInlineRenderCallback');
+ // Note: we are intentionally passing an inline arrow. Don't refactor.
+ return {value => renderChildValue(value)};
+ }
- function Provider(props) {
- return (
-
- {props.children}
-
- );
- }
+ function ChildWithCachedRenderCallback() {
+ ReactNoop.yield('ChildWithCachedRenderCallback');
+ return {renderChildValue};
+ }
- function Foo(props) {
- return (
-
- {value => {
- ReactNoop.yield('Foo');
+ class PureIndirection extends React.PureComponent {
+ render() {
+ ReactNoop.yield('PureIndirection');
return (
-
- {props.children && props.children()}
+
+
);
- }}
-
- );
- }
+ }
+ }
- function Bar(props) {
- return (
-
- {value => {
- ReactNoop.yield('Bar');
+ class App extends React.Component {
+ render() {
+ ReactNoop.yield('App');
return (
-
-
- {props.children && props.children()}
-
+
+
+
);
- }}
-
- );
- }
+ }
+ }
- class Indirection extends React.Component {
- shouldComponentUpdate() {
- return false;
- }
- render() {
- return this.props.children;
- }
- }
+ // Initial mount
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([
+ 'App',
+ 'PureIndirection',
+ 'ChildWithInlineRenderCallback',
+ 'Consumer',
+ 'ChildWithCachedRenderCallback',
+ 'Consumer',
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
+
+ // Update (bailout)
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['App']);
+ expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
+
+ // Update (no bailout)
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']);
+ expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
+ });
+
+ // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
+ // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
+ // If we bailed out on referential equality, it would be confusing that you
+ // can call this.setState(), but an autobound render callback "blocked" the update.
+ // https://github.com/facebook/react/pull/12470#issuecomment-376917711
+ it('consumer does not bail out if there were no bailouts above it', () => {
+ const Context = React.createContext(0);
+ const Consumer = getConsumer(Context);
+
+ class App extends React.Component {
+ state = {
+ text: 'hello',
+ };
+
+ renderConsumer = context => {
+ ReactNoop.yield('App#renderConsumer');
+ return ;
+ };
+
+ render() {
+ ReactNoop.yield('App');
+ return (
+
+ {this.renderConsumer}
+
+ );
+ }
+ }
- function App(props) {
- return (
-
-
-
- {/* Use a render prop so we don't test constant elements. */}
- {() => (
-
-
- {() => (
-
-
-
- )}
-
-
- )}
-
-
-
- );
- }
+ // Initial mount
+ let inst;
+ ReactNoop.render( (inst = ref)} />);
+ expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
+ expect(ReactNoop.getChildren()).toEqual([span('hello')]);
+
+ // Update
+ inst.setState({text: 'goodbye'});
+ expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
+ expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
+ });
+
+ // This is a regression case for https://github.com/facebook/react/issues/12389.
+ it('does not run into an infinite loop', () => {
+ const Context = React.createContext(null);
+ const Consumer = getConsumer(Context);
+
+ class App extends React.Component {
+ renderItem(id) {
+ return (
+
+ {() => inner}
+ outer
+
+ );
+ }
+ renderList() {
+ const list = [1, 2].map(id => this.renderItem(id));
+ if (this.props.reverse) {
+ list.reverse();
+ }
+ return list;
+ }
+ render() {
+ return (
+
+ {this.renderList()}
+
+ );
+ }
+ }
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
- expect(ReactNoop.getChildren()).toEqual([
- span('Foo: 1'),
- span('Bar: 1'),
- span('Foo: 1'),
- ]);
-
- // Update only foo
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Foo', 'Foo']);
- expect(ReactNoop.getChildren()).toEqual([
- span('Foo: 2'),
- span('Bar: 1'),
- span('Foo: 2'),
- ]);
-
- // Update only bar
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Bar']);
- expect(ReactNoop.getChildren()).toEqual([
- span('Foo: 2'),
- span('Bar: 2'),
- span('Foo: 2'),
- ]);
-
- // Update both
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar', 'Foo']);
- expect(ReactNoop.getChildren()).toEqual([
- span('Foo: 3'),
- span('Bar: 3'),
- span('Foo: 3'),
- ]);
- });
+ ReactNoop.render();
+ ReactNoop.flush();
+ ReactNoop.render();
+ ReactNoop.flush();
+ ReactNoop.render();
+ ReactNoop.flush();
+ });
- it('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
- spyOnDev(console, 'error');
+ // This is a regression case for https://github.com/facebook/react/issues/12686
+ it('does not skip some siblings', () => {
+ const Context = React.createContext(0);
+ const ContextConsumer = getConsumer(Context);
- const Context = React.createContext(
- 0,
- (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
- );
+ class App extends React.Component {
+ state = {
+ step: 0,
+ };
- ReactNoop.render();
- ReactNoop.flush();
+ render() {
+ ReactNoop.yield('App');
+ return (
+
+
+ {this.state.step > 0 && }
+
+ );
+ }
+ }
- // Update
- ReactNoop.render();
- ReactNoop.flush();
+ class StaticContent extends React.PureComponent {
+ render() {
+ return (
+
+
+
+
+
+
+ );
+ }
+ }
- if (__DEV__) {
- expect(console.error).toHaveBeenCalledTimes(1);
- expect(console.error.calls.argsFor(0)[0]).toContain(
- 'calculateChangedBits: Expected the return value to be a 31-bit ' +
- 'integer. Instead received: 4294967295',
- );
- }
- });
+ class Indirection extends React.PureComponent {
+ render() {
+ return (
+
+ {value => {
+ ReactNoop.yield('Consumer');
+ return ;
+ }}
+
+ );
+ }
+ }
- it('warns if multiple renderers concurrently render the same context', () => {
- spyOnDev(console, 'error');
- const Context = React.createContext(0);
+ // Initial mount
+ let inst;
+ ReactNoop.render( (inst = ref)} />);
+ expect(ReactNoop.flush()).toEqual(['App']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('static 1'),
+ span('static 2'),
+ ]);
+ // Update the first time
+ inst.setState({step: 1});
+ expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('static 1'),
+ span('static 2'),
+ span(1),
+ ]);
+ // Update the second time
+ inst.setState({step: 2});
+ expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('static 1'),
+ span('static 2'),
+ span(2),
+ ]);
+ });
+ });
+ }
- function Foo(props) {
- ReactNoop.yield('Foo');
- return null;
- }
+ describe('Context.Provider', () => {
+ it('warns if calculateChangedBits returns larger than a 31-bit integer', () => {
+ spyOnDev(console, 'error');
- function App(props) {
- return (
-
-
-
-
+ const Context = React.createContext(
+ 0,
+ (a, b) => Math.pow(2, 32) - 1, // Return 32 bit int
);
- }
- ReactNoop.render();
- // Render past the Provider, but don't commit yet
- ReactNoop.flushThrough(['Foo']);
+ ReactNoop.render();
+ ReactNoop.flush();
- // Get a new copy of ReactNoop
- jest.resetModules();
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
- React = require('react');
- ReactNoop = require('react-noop-renderer');
+ // Update
+ ReactNoop.render();
+ ReactNoop.flush();
- // 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',
- );
- }
- });
+ if (__DEV__) {
+ expect(console.error).toHaveBeenCalledTimes(1);
+ expect(console.error.calls.argsFor(0)[0]).toContain(
+ 'calculateChangedBits: Expected the return value to be a 31-bit ' +
+ 'integer. Instead received: 4294967295',
+ );
+ }
+ });
- it('warns if consumer child is not a function', () => {
- spyOnDev(console, 'error');
- const Context = React.createContext(0);
- ReactNoop.render();
- expect(ReactNoop.flush).toThrow('render is not a function');
- if (__DEV__) {
- expect(console.error.calls.argsFor(0)[0]).toContain(
- 'A context consumer was rendered with multiple children, or a child ' +
- "that isn't a function",
- );
- }
- });
+ it('warns if multiple renderers concurrently render the same context', () => {
+ spyOnDev(console, 'error');
+ const Context = React.createContext(0);
- it("does not re-render if there's an update in a child", () => {
- const Context = React.createContext(0);
+ function Foo(props) {
+ ReactNoop.yield('Foo');
+ return null;
+ }
- let child;
- class Child extends React.Component {
- state = {step: 0};
- render() {
- ReactNoop.yield('Child');
+ function App(props) {
return (
-
+
+
+
+
);
}
- }
- function App(props) {
- return (
-
-
- {value => {
- ReactNoop.yield('Consumer render prop');
- return (child = inst)} context={value} />;
- }}
-
-
- );
- }
+ ReactNoop.render();
+ // Render past the Provider, but don't commit yet
+ ReactNoop.flushThrough(['Foo']);
- // Initial mount
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['Consumer render prop', 'Child']);
- expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 0')]);
+ // Get a new copy of ReactNoop
+ jest.resetModules();
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
- child.setState({step: 1});
- expect(ReactNoop.flush()).toEqual(['Child']);
- expect(ReactNoop.getChildren()).toEqual([span('Context: 1, Step: 1')]);
- });
-
- it('provider bails out if children and value are unchanged (like sCU)', () => {
- const Context = React.createContext(0);
-
- function Child() {
- ReactNoop.yield('Child');
- return ;
- }
-
- const children = ;
-
- function App(props) {
- ReactNoop.yield('App');
- return (
- {children}
- );
- }
+ // Render the provider again using a different renderer
+ ReactNoop.render();
+ ReactNoop.flush();
- // Initial mount
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['App', 'Child']);
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
-
- // Update
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual([
- 'App',
- // Child does not re-render
- ]);
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
- });
-
- it('provider does not bail out if legacy context changed above', () => {
- const Context = React.createContext(0);
-
- function Child() {
- ReactNoop.yield('Child');
- return ;
- }
+ if (__DEV__) {
+ expect(console.error.calls.argsFor(0)[0]).toContain(
+ 'Detected multiple renderers concurrently rendering the same ' +
+ 'context provider. This is currently unsupported',
+ );
+ }
+ });
- const children = ;
+ it('provider bails out if children and value are unchanged (like sCU)', () => {
+ const Context = React.createContext(0);
- class LegacyProvider extends React.Component {
- static childContextTypes = {
- legacyValue: () => {},
- };
- state = {legacyValue: 1};
- getChildContext() {
- return {legacyValue: this.state.legacyValue};
- }
- render() {
- ReactNoop.yield('LegacyProvider');
- return this.props.children;
+ function Child() {
+ ReactNoop.yield('Child');
+ return ;
}
- }
- class App extends React.Component {
- state = {value: 1};
- render() {
+ const children = ;
+
+ function App(props) {
ReactNoop.yield('App');
return (
-
- {this.props.children}
-
+ {children}
);
}
- }
- const legacyProviderRef = React.createRef();
- const appRef = React.createRef();
-
- // Initial mount
- ReactNoop.render(
-
-
- {children}
-
- ,
- );
- expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
-
- // Update App with same value (should bail out)
- appRef.current.setState({value: 1});
- expect(ReactNoop.flush()).toEqual(['App']);
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
-
- // Update LegacyProvider (should not bail out)
- legacyProviderRef.current.setState({value: 1});
- expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
-
- // Update App with same value (should bail out)
- appRef.current.setState({value: 1});
- expect(ReactNoop.flush()).toEqual(['App']);
- expect(ReactNoop.getChildren()).toEqual([span('Child')]);
- });
+ // Initial mount
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['App', 'Child']);
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
+
+ // Update
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([
+ 'App',
+ // Child does not re-render
+ ]);
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
+ });
- it('consumer bails out if value is unchanged and something above bailed out', () => {
- const Context = React.createContext(0);
+ it('provider does not bail out if legacy context changed above', () => {
+ const Context = React.createContext(0);
- function renderChildValue(value) {
- ReactNoop.yield('Consumer');
- return ;
- }
+ function Child() {
+ ReactNoop.yield('Child');
+ return ;
+ }
- function ChildWithInlineRenderCallback() {
- ReactNoop.yield('ChildWithInlineRenderCallback');
- // Note: we are intentionally passing an inline arrow. Don't refactor.
- return (
- {value => renderChildValue(value)}
- );
- }
+ const children = ;
- function ChildWithCachedRenderCallback() {
- ReactNoop.yield('ChildWithCachedRenderCallback');
- return {renderChildValue};
- }
-
- class PureIndirection extends React.PureComponent {
- render() {
- ReactNoop.yield('PureIndirection');
- return (
-
-
-
-
- );
+ class LegacyProvider extends React.Component {
+ static childContextTypes = {
+ legacyValue: () => {},
+ };
+ state = {legacyValue: 1};
+ getChildContext() {
+ return {legacyValue: this.state.legacyValue};
+ }
+ render() {
+ ReactNoop.yield('LegacyProvider');
+ return this.props.children;
+ }
}
- }
- class App extends React.Component {
- render() {
- ReactNoop.yield('App');
- return (
-
-
-
- );
+ class App extends React.Component {
+ state = {value: 1};
+ render() {
+ ReactNoop.yield('App');
+ return (
+
+ {this.props.children}
+
+ );
+ }
}
- }
- // Initial mount
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual([
- 'App',
- 'PureIndirection',
- 'ChildWithInlineRenderCallback',
- 'Consumer',
- 'ChildWithCachedRenderCallback',
- 'Consumer',
- ]);
- expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
-
- // Update (bailout)
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['App']);
- expect(ReactNoop.getChildren()).toEqual([span(1), span(1)]);
-
- // Update (no bailout)
- ReactNoop.render();
- expect(ReactNoop.flush()).toEqual(['App', 'Consumer', 'Consumer']);
- expect(ReactNoop.getChildren()).toEqual([span(2), span(2)]);
- });
+ const legacyProviderRef = React.createRef();
+ const appRef = React.createRef();
- // Context consumer bails out on propagating "deep" updates when `value` hasn't changed.
- // However, it doesn't bail out from rendering if the component above it re-rendered anyway.
- // If we bailed out on referential equality, it would be confusing that you
- // can call this.setState(), but an autobound render callback "blocked" the update.
- // https://github.com/facebook/react/pull/12470#issuecomment-376917711
- it('consumer does not bail out if there were no bailouts above it', () => {
- const Context = React.createContext(0);
-
- class App extends React.Component {
- state = {
- text: 'hello',
- };
-
- renderConsumer = context => {
- ReactNoop.yield('App#renderConsumer');
- return ;
- };
+ // Initial mount
+ ReactNoop.render(
+
+
+ {children}
+
+ ,
+ );
+ expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
+
+ // Update App with same value (should bail out)
+ appRef.current.setState({value: 1});
+ expect(ReactNoop.flush()).toEqual(['App']);
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
+
+ // Update LegacyProvider (should not bail out)
+ legacyProviderRef.current.setState({value: 1});
+ expect(ReactNoop.flush()).toEqual(['LegacyProvider', 'App', 'Child']);
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
+
+ // Update App with same value (should bail out)
+ appRef.current.setState({value: 1});
+ expect(ReactNoop.flush()).toEqual(['App']);
+ expect(ReactNoop.getChildren()).toEqual([span('Child')]);
+ });
+ });
- render() {
- ReactNoop.yield('App');
- return (
-
- {this.renderConsumer}
-
+ describe('Context.Consumer', () => {
+ it('warns if child is not a function', () => {
+ spyOnDev(console, 'error');
+ const Context = React.createContext(0);
+ ReactNoop.render();
+ expect(ReactNoop.flush).toThrow('render is not a function');
+ if (__DEV__) {
+ expect(console.error.calls.argsFor(0)[0]).toContain(
+ 'A context consumer was rendered with multiple children, or a child ' +
+ "that isn't a function",
);
}
- }
-
- // Initial mount
- let inst;
- ReactNoop.render( (inst = ref)} />);
- expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
- expect(ReactNoop.getChildren()).toEqual([span('hello')]);
-
- // Update
- inst.setState({text: 'goodbye'});
- expect(ReactNoop.flush()).toEqual(['App', 'App#renderConsumer']);
- expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
- });
+ });
- // This is a regression case for https://github.com/facebook/react/issues/12389.
- it('does not run into an infinite loop', () => {
- const Context = React.createContext(null);
+ it('can read other contexts inside consumer render prop', () => {
+ const FooContext = React.createContext(0);
+ const BarContext = React.createContext(0);
- class App extends React.Component {
- renderItem(id) {
+ function FooAndBar() {
return (
-
- {() => inner}
- outer
-
+
+ {foo => {
+ const bar = BarContext.unstable_read();
+ return ;
+ }}
+
);
}
- renderList() {
- const list = [1, 2].map(id => this.renderItem(id));
- if (this.props.reverse) {
- list.reverse();
+
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ return this.props.children;
}
- return list;
}
- render() {
+
+ function App(props) {
return (
- {this.renderList()}
+
+
+
+
+
+
+
);
}
- }
- ReactNoop.render();
- ReactNoop.flush();
- ReactNoop.render();
- ReactNoop.flush();
- ReactNoop.render();
- ReactNoop.flush();
- });
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1']);
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 1, Bar: 1')]);
- // This is a regression case for https://github.com/facebook/react/issues/12686
- it('does not skip some siblings', () => {
- const Context = React.createContext(0);
+ // Update foo
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']);
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 1')]);
- class App extends React.Component {
- state = {
- step: 0,
- };
+ // Update bar
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
+ expect(ReactNoop.getChildren()).toEqual([span('Foo: 2, Bar: 2')]);
+ });
+ });
- render() {
- ReactNoop.yield('App');
+ describe('unstable_readContext', () => {
+ it('can use the same context multiple times in the same function', () => {
+ const Context = React.createContext({foo: 0, bar: 0, baz: 0}, (a, b) => {
+ let result = 0;
+ if (a.foo !== b.foo) {
+ result |= 0b001;
+ }
+ if (a.bar !== b.bar) {
+ result |= 0b010;
+ }
+ if (a.baz !== b.baz) {
+ result |= 0b100;
+ }
+ return result;
+ });
+
+ function Provider(props) {
return (
-
-
- {this.state.step > 0 && }
+
+ {props.children}
);
}
- }
- class StaticContent extends React.PureComponent {
- render() {
- return (
-
-
-
-
-
-
- );
+ function FooAndBar() {
+ const {foo} = Context.unstable_read(0b001);
+ const {bar} = Context.unstable_read(0b010);
+ return ;
}
- }
- class Indirection extends React.PureComponent {
- render() {
- return ;
+ function Baz() {
+ const {baz} = Context.unstable_read(0b100);
+ return ;
}
- }
- function Consumer() {
- return (
-
- {value => {
- ReactNoop.yield('Consumer');
- return ;
- }}
-
- );
- }
+ class Indirection extends React.Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+ render() {
+ return this.props.children;
+ }
+ }
+
+ function App(props) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
- // Initial mount
- let inst;
- ReactNoop.render( (inst = ref)} />);
- expect(ReactNoop.flush()).toEqual(['App']);
- expect(ReactNoop.getChildren()).toEqual([
- span('static 1'),
- span('static 2'),
- ]);
- // Update the first time
- inst.setState({step: 1});
- expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
- expect(ReactNoop.getChildren()).toEqual([
- span('static 1'),
- span('static 2'),
- span(1),
- ]);
- // Update the second time
- inst.setState({step: 2});
- expect(ReactNoop.flush()).toEqual(['App', 'Consumer']);
- expect(ReactNoop.getChildren()).toEqual([
- span('static 1'),
- span('static 2'),
- span(2),
- ]);
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo: 1, Bar: 1', 'Baz: 1']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 1, Bar: 1'),
+ span('Baz: 1'),
+ ]);
+
+ // Update only foo
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 1']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 2, Bar: 1'),
+ span('Baz: 1'),
+ ]);
+
+ // Update only bar
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Foo: 2, Bar: 2']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 2, Bar: 2'),
+ span('Baz: 1'),
+ ]);
+
+ // Update only baz
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual(['Baz: 2']);
+ expect(ReactNoop.getChildren()).toEqual([
+ span('Foo: 2, Bar: 2'),
+ span('Baz: 2'),
+ ]);
+ });
});
describe('fuzz test', () => {
diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js
index 4c1b06bfed5d3..8a972139b6e36 100644
--- a/packages/react/src/ReactContext.js
+++ b/packages/react/src/ReactContext.js
@@ -11,8 +11,24 @@ import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
import type {ReactContext} from 'shared/ReactTypes';
+import invariant from 'shared/invariant';
import warning from 'shared/warning';
+import ReactCurrentOwner from './ReactCurrentOwner';
+
+export function readContext(
+ context: ReactContext,
+ observedBits: void | number | boolean,
+): T {
+ const dispatcher = ReactCurrentOwner.currentDispatcher;
+ invariant(
+ dispatcher !== null,
+ 'Context.unstable_read(): Context can only be read while React is ' +
+ 'rendering, e.g. inside the render method or getDerivedStateFromProps.',
+ );
+ return dispatcher.readContext(context, observedBits);
+}
+
export function createContext(
defaultValue: T,
calculateChangedBits: ?(a: T, b: T) => number,
@@ -47,6 +63,7 @@ export function createContext(
// These are circular
Provider: (null: any),
Consumer: (null: any),
+ unstable_read: (null: any),
};
context.Provider = {
@@ -54,6 +71,7 @@ export function createContext(
_context: context,
};
context.Consumer = context;
+ context.unstable_read = readContext.bind(null, context);
if (__DEV__) {
context._currentRenderer = null;
diff --git a/packages/react/src/ReactCurrentOwner.js b/packages/react/src/ReactCurrentOwner.js
index 72ed4e2eb8475..89cd104ca6a9d 100644
--- a/packages/react/src/ReactCurrentOwner.js
+++ b/packages/react/src/ReactCurrentOwner.js
@@ -8,6 +8,7 @@
*/
import type {Fiber} from 'react-reconciler/src/ReactFiber';
+import typeof {Dispatcher} from 'react-reconciler/src/ReactFiberDispatcher';
/**
* Keeps track of the current owner.
@@ -21,6 +22,7 @@ const ReactCurrentOwner = {
* @type {ReactComponent}
*/
current: (null: null | Fiber),
+ currentDispatcher: (null: null | Dispatcher),
};
export default ReactCurrentOwner;
diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js
index ac4eed362d0ad..bff9413c82854 100644
--- a/packages/shared/ReactTypes.js
+++ b/packages/shared/ReactTypes.js
@@ -79,6 +79,7 @@ export type ReactContext = {
$$typeof: Symbol | number,
Consumer: ReactContext,
Provider: ReactProviderType,
+ unstable_read: () => T,
_calculateChangedBits: ((a: T, b: T) => number) | null,
_defaultValue: T,