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,