diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index e94ae8109781a..61c75d05cddc6 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -59,7 +59,6 @@ import warningWithoutStack from 'shared/warningWithoutStack'; import * as ReactCurrentFiber from './ReactCurrentFiber'; import {cancelWorkTimer} from './ReactDebugFiberPerf'; -import {applyDerivedStateFromProps} from './ReactFiberClassComponent'; import { mountChildFibers, reconcileChildFibers, @@ -97,6 +96,7 @@ import { } from './ReactFiberHydrationContext'; import { adoptClassInstance, + applyDerivedStateFromProps, constructClassInstance, mountClassInstance, resumeMountClassInstance, @@ -109,11 +109,13 @@ import {resolveLazyComponentTag} from './ReactFiber'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; let didWarnAboutBadClass; +let didWarnAboutContextTypeOnFunctionalComponent; let didWarnAboutGetDerivedStateOnFunctionalComponent; let didWarnAboutStatelessRefs; if (__DEV__) { didWarnAboutBadClass = {}; + didWarnAboutContextTypeOnFunctionalComponent = {}; didWarnAboutGetDerivedStateOnFunctionalComponent = {}; didWarnAboutStatelessRefs = {}; } @@ -805,6 +807,22 @@ function mountIndeterminateComponent( ] = true; } } + + if ( + typeof Component.contextType === 'object' && + Component.contextType !== null + ) { + const componentName = getComponentName(Component) || 'Unknown'; + + if (!didWarnAboutContextTypeOnFunctionalComponent[componentName]) { + warningWithoutStack( + false, + '%s: Stateless functional components do not support contextType.', + componentName, + ); + didWarnAboutContextTypeOnFunctionalComponent[componentName] = true; + } + } } reconcileChildren(current, workInProgress, value, renderExpirationTime); memoizeProps(workInProgress, props); diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 48b29e7917f8e..ee0da92c911fc 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -65,6 +65,8 @@ let didWarnAboutUndefinedDerivedState; let warnOnUndefinedDerivedState; let warnOnInvalidCallback; let didWarnAboutDirectlyAssigningPropsToState; +let didWarnAboutContextTypeAndContextTypes; +let didWarnAboutInvalidateContextType; if (__DEV__) { didWarnAboutStateAssignmentForComponent = new Set(); @@ -73,6 +75,8 @@ if (__DEV__) { didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); didWarnAboutDirectlyAssigningPropsToState = new Set(); didWarnAboutUndefinedDerivedState = new Set(); + didWarnAboutContextTypeAndContextTypes = new Set(); + didWarnAboutInvalidateContextType = new Set(); const didWarnOnInvalidCallback = new Set(); @@ -234,7 +238,7 @@ function checkShouldComponentUpdate( newProps, oldState, newState, - nextLegacyContext, + nextContext, ) { const instance = workInProgress.stateNode; if (typeof instance.shouldComponentUpdate === 'function') { @@ -242,7 +246,7 @@ function checkShouldComponentUpdate( const shouldUpdate = instance.shouldComponentUpdate( newProps, newState, - nextLegacyContext, + nextContext, ); stopPhaseTimer(); @@ -319,6 +323,13 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { 'property to define propTypes instead.', name, ); + const noInstanceContextType = !instance.contextType; + warningWithoutStack( + noInstanceContextType, + 'contextType was defined as an instance property on %s. Use a static ' + + 'property to define contextType instead.', + name, + ); const noInstanceContextTypes = !instance.contextTypes; warningWithoutStack( noInstanceContextTypes, @@ -326,6 +337,36 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { 'property to define contextTypes instead.', name, ); + + if ( + ctor.contextType && + ctor.contextTypes && + !didWarnAboutContextTypeAndContextTypes.has(ctor) + ) { + didWarnAboutContextTypeAndContextTypes.add(ctor); + warningWithoutStack( + false, + '%s declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.', + name, + ); + } + + if ( + ctor.contextType && + typeof ctor.contextType.unstable_read !== 'function' && + !didWarnAboutInvalidateContextType.has(ctor) + ) { + didWarnAboutInvalidateContextType.add(ctor); + warningWithoutStack( + false, + '%s defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'Did you accidentally pass the Context.Provider instead?', + name, + ); + } + const noComponentShouldUpdate = typeof instance.componentShouldUpdate !== 'function'; warningWithoutStack( @@ -475,12 +516,25 @@ function constructClassInstance( props: any, renderExpirationTime: ExpirationTime, ): any { - const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); - const contextTypes = ctor.contextTypes; - const isContextConsumer = contextTypes !== null && contextTypes !== undefined; - const context = isContextConsumer - ? getMaskedContext(workInProgress, unmaskedContext) - : emptyContextObject; + let isLegacyContextConsumer = false; + let unmaskedContext = emptyContextObject; + let context = null; + const contextType = ctor.contextType; + if ( + typeof contextType === 'object' && + contextType !== null && + typeof contextType.unstable_read === 'function' + ) { + context = (contextType: any).unstable_read(); + } else { + unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); + const contextTypes = ctor.contextTypes; + isLegacyContextConsumer = + contextTypes !== null && contextTypes !== undefined; + context = isLegacyContextConsumer + ? getMaskedContext(workInProgress, unmaskedContext) + : emptyContextObject; + } // Instantiate twice to help detect side-effects. if (__DEV__) { @@ -587,7 +641,7 @@ function constructClassInstance( // Cache unmasked context so we can avoid recreating masked context unless necessary. // ReactFiberContext usually updates this cache but can't for newly-created instances. - if (isContextConsumer) { + if (isLegacyContextConsumer) { cacheContext(workInProgress, unmaskedContext, context); } @@ -625,15 +679,15 @@ function callComponentWillReceiveProps( workInProgress, instance, newProps, - nextLegacyContext, + nextContext, ) { const oldState = instance.state; startPhaseTimer(workInProgress, 'componentWillReceiveProps'); if (typeof instance.componentWillReceiveProps === 'function') { - instance.componentWillReceiveProps(newProps, nextLegacyContext); + instance.componentWillReceiveProps(newProps, nextContext); } if (typeof instance.UNSAFE_componentWillReceiveProps === 'function') { - instance.UNSAFE_componentWillReceiveProps(newProps, nextLegacyContext); + instance.UNSAFE_componentWillReceiveProps(newProps, nextContext); } stopPhaseTimer(); @@ -668,12 +722,21 @@ function mountClassInstance( } const instance = workInProgress.stateNode; - const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); - instance.props = newProps; instance.state = workInProgress.memoizedState; instance.refs = emptyRefsObject; - instance.context = getMaskedContext(workInProgress, unmaskedContext); + + const contextType = ctor.contextType; + if ( + typeof contextType === 'object' && + contextType !== null && + typeof contextType.unstable_read === 'function' + ) { + instance.context = (contextType: any).unstable_read(); + } else { + const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); + instance.context = getMaskedContext(workInProgress, unmaskedContext); + } if (__DEV__) { if (instance.state === newProps) { @@ -774,15 +837,22 @@ function resumeMountClassInstance( instance.props = oldProps; const oldContext = instance.context; - const nextLegacyUnmaskedContext = getUnmaskedContext( - workInProgress, - ctor, - true, - ); - const nextLegacyContext = getMaskedContext( - workInProgress, - nextLegacyUnmaskedContext, - ); + const contextType = ctor.contextType; + let nextContext; + if ( + typeof contextType === 'object' && + contextType !== null && + typeof contextType.unstable_read === 'function' + ) { + nextContext = (contextType: any).unstable_read(); + } else { + const nextLegacyUnmaskedContext = getUnmaskedContext( + workInProgress, + ctor, + true, + ); + nextContext = getMaskedContext(workInProgress, nextLegacyUnmaskedContext); + } const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = @@ -800,12 +870,12 @@ function resumeMountClassInstance( (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function') ) { - if (oldProps !== newProps || oldContext !== nextLegacyContext) { + if (oldProps !== newProps || oldContext !== nextContext) { callComponentWillReceiveProps( workInProgress, instance, newProps, - nextLegacyContext, + nextContext, ); } } @@ -858,7 +928,7 @@ function resumeMountClassInstance( newProps, oldState, newState, - nextLegacyContext, + nextContext, ); if (shouldUpdate) { @@ -898,7 +968,7 @@ function resumeMountClassInstance( // if shouldComponentUpdate returns false. instance.props = newProps; instance.state = newState; - instance.context = nextLegacyContext; + instance.context = nextContext; return shouldUpdate; } @@ -917,15 +987,18 @@ function updateClassInstance( instance.props = oldProps; const oldContext = instance.context; - const nextLegacyUnmaskedContext = getUnmaskedContext( - workInProgress, - ctor, - true, - ); - const nextLegacyContext = getMaskedContext( - workInProgress, - nextLegacyUnmaskedContext, - ); + const contextType = ctor.contextType; + let nextContext; + if ( + typeof contextType === 'object' && + contextType !== null && + typeof contextType.unstable_read === 'function' + ) { + nextContext = (contextType: any).unstable_read(); + } else { + const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true); + nextContext = getMaskedContext(workInProgress, nextUnmaskedContext); + } const getDerivedStateFromProps = ctor.getDerivedStateFromProps; const hasNewLifecycles = @@ -943,12 +1016,12 @@ function updateClassInstance( (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function') ) { - if (oldProps !== newProps || oldContext !== nextLegacyContext) { + if (oldProps !== newProps || oldContext !== nextContext) { callComponentWillReceiveProps( workInProgress, instance, newProps, - nextLegacyContext, + nextContext, ); } } @@ -1015,7 +1088,7 @@ function updateClassInstance( newProps, oldState, newState, - nextLegacyContext, + nextContext, ); if (shouldUpdate) { @@ -1028,14 +1101,10 @@ function updateClassInstance( ) { startPhaseTimer(workInProgress, 'componentWillUpdate'); if (typeof instance.componentWillUpdate === 'function') { - instance.componentWillUpdate(newProps, newState, nextLegacyContext); + instance.componentWillUpdate(newProps, newState, nextContext); } if (typeof instance.UNSAFE_componentWillUpdate === 'function') { - instance.UNSAFE_componentWillUpdate( - newProps, - newState, - nextLegacyContext, - ); + instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext); } stopPhaseTimer(); } @@ -1075,7 +1144,7 @@ function updateClassInstance( // if shouldComponentUpdate returns false. instance.props = newProps; instance.state = newState; - instance.context = nextLegacyContext; + instance.context = nextContext; return shouldUpdate; } diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee index 88faa12d2d5a8..ec0921293d53e 100644 --- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee @@ -392,6 +392,7 @@ describe 'ReactCoffeeScriptClass', -> class Foo extends React.Component constructor: -> @contextTypes = {} + @contextType = {} @propTypes = {} getInitialState: -> @@ -413,6 +414,7 @@ describe 'ReactCoffeeScriptClass', -> 'getDefaultProps was defined on Foo, a plain JavaScript class.', 'propTypes was defined as an instance property on Foo.', 'contextTypes was defined as an instance property on Foo.', + 'contextType was defined as an instance property on Foo.', ], {withoutStack: true}) expect(getInitialStateWasCalled).toBe false expect(getDefaultPropsWasCalled).toBe false diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index be6ce254b4539..97ac4e339cbc4 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -67,9 +67,16 @@ describe('ReactContextValidator', () => { }); it('should pass next context to lifecycles', () => { - let actualComponentWillReceiveProps; - let actualShouldComponentUpdate; - let actualComponentWillUpdate; + let componentDidMountContext; + let componentDidUpdateContext; + let componentWillReceivePropsContext; + let componentWillReceivePropsNextContext; + let componentWillUpdateContext; + let componentWillUpdateNextContext; + let constructorContext; + let renderContext; + let shouldComponentUpdateContext; + let shouldComponentUpdateNextContext; class Parent extends React.Component { getChildContext() { @@ -78,7 +85,6 @@ describe('ReactContextValidator', () => { bar: 'bar', }; } - render() { return ; } @@ -89,23 +95,34 @@ describe('ReactContextValidator', () => { }; class Component extends React.Component { + constructor(props, context) { + super(props, context); + constructorContext = context; + } UNSAFE_componentWillReceiveProps(nextProps, nextContext) { - actualComponentWillReceiveProps = nextContext; + componentWillReceivePropsContext = this.context; + componentWillReceivePropsNextContext = nextContext; return true; } - shouldComponentUpdate(nextProps, nextState, nextContext) { - actualShouldComponentUpdate = nextContext; + shouldComponentUpdateContext = this.context; + shouldComponentUpdateNextContext = nextContext; return true; } - UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) { - actualComponentWillUpdate = nextContext; + componentWillUpdateContext = this.context; + componentWillUpdateNextContext = nextContext; } - render() { + renderContext = this.context; return
; } + componentDidMount() { + componentDidMountContext = this.context; + } + componentDidUpdate() { + componentDidUpdateContext = this.context; + } } Component.contextTypes = { foo: PropTypes.string, @@ -113,10 +130,18 @@ describe('ReactContextValidator', () => { const container = document.createElement('div'); ReactDOM.render(, container); + expect(constructorContext).toEqual({foo: 'abc'}); + expect(renderContext).toEqual({foo: 'abc'}); + expect(componentDidMountContext).toEqual({foo: 'abc'}); ReactDOM.render(, container); - expect(actualComponentWillReceiveProps).toEqual({foo: 'def'}); - expect(actualShouldComponentUpdate).toEqual({foo: 'def'}); - expect(actualComponentWillUpdate).toEqual({foo: 'def'}); + expect(componentWillReceivePropsContext).toEqual({foo: 'abc'}); + expect(componentWillReceivePropsNextContext).toEqual({foo: 'def'}); + expect(shouldComponentUpdateContext).toEqual({foo: 'abc'}); + expect(shouldComponentUpdateNextContext).toEqual({foo: 'def'}); + expect(componentWillUpdateContext).toEqual({foo: 'abc'}); + expect(componentWillUpdateNextContext).toEqual({foo: 'def'}); + expect(renderContext).toEqual({foo: 'def'}); + expect(componentDidUpdateContext).toEqual({foo: 'def'}); }); it('should check context types', () => { @@ -338,4 +363,207 @@ describe('ReactContextValidator', () => { expect(childContext.bar).toBeUndefined(); expect(childContext.foo).toBe('FOO'); }); + + it('should pass next context to lifecycles', () => { + let componentDidMountContext; + let componentDidUpdateContext; + let componentWillReceivePropsContext; + let componentWillReceivePropsNextContext; + let componentWillUpdateContext; + let componentWillUpdateNextContext; + let constructorContext; + let renderContext; + let shouldComponentUpdateWasCalled = false; + + const Context = React.createContext(); + + class Component extends React.Component { + static contextType = Context; + constructor(props, context) { + super(props, context); + constructorContext = context; + } + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + componentWillReceivePropsContext = this.context; + componentWillReceivePropsNextContext = nextContext; + return true; + } + shouldComponentUpdate(nextProps, nextState, nextContext) { + shouldComponentUpdateWasCalled = true; + return true; + } + UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) { + componentWillUpdateContext = this.context; + componentWillUpdateNextContext = nextContext; + } + render() { + renderContext = this.context; + return
; + } + componentDidMount() { + componentDidMountContext = this.context; + } + componentDidUpdate() { + componentDidUpdateContext = this.context; + } + } + + const firstContext = {foo: 123}; + const secondContext = {bar: 456}; + + const container = document.createElement('div'); + ReactDOM.render( + + + , + container, + ); + expect(constructorContext).toBe(firstContext); + expect(renderContext).toBe(firstContext); + expect(componentDidMountContext).toBe(firstContext); + ReactDOM.render( + + + , + container, + ); + expect(componentWillReceivePropsContext).toBe(firstContext); + expect(componentWillReceivePropsNextContext).toBe(secondContext); + expect(componentWillUpdateContext).toBe(firstContext); + expect(componentWillUpdateNextContext).toBe(secondContext); + expect(renderContext).toBe(secondContext); + expect(componentDidUpdateContext).toBe(secondContext); + + // sCU is not called in this case because React force updates when a provider re-renders + expect(shouldComponentUpdateWasCalled).toBe(false); + }); + + it('should warn if both contextType and contextTypes are defined', () => { + const Context = React.createContext(); + + class ParentContextProvider extends React.Component { + static childContextTypes = { + foo: PropTypes.string, + }; + getChildContext() { + return { + foo: 'FOO', + }; + } + render() { + return this.props.children; + } + } + + class ComponentA extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; + static contextType = Context; + render() { + return
; + } + } + class ComponentB extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; + static contextType = Context; + render() { + return
; + } + } + + expect(() => + ReactTestUtils.renderIntoDocument( + + + , + ), + ).toWarnDev( + 'Warning: ComponentA declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.', + {withoutStack: true}, + ); + + // Warnings should be deduped by component type + ReactTestUtils.renderIntoDocument( + + + , + ); + + expect(() => + ReactTestUtils.renderIntoDocument( + + + , + ), + ).toWarnDev( + 'Warning: ComponentB declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.', + {withoutStack: true}, + ); + }); + + it('should warn if an invalid contextType is defined', () => { + const Context = React.createContext(); + + class ComponentA extends React.Component { + static contextType = Context.Provider; + render() { + return
; + } + } + class ComponentB extends React.Component { + static contextType = Context.Provider; + render() { + return
; + } + } + + expect(() => ReactTestUtils.renderIntoDocument()).toWarnDev( + 'Warning: ComponentA defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'Did you accidentally pass the Context.Provider instead?', + {withoutStack: true}, + ); + + // Warnings should be deduped by component type + ReactTestUtils.renderIntoDocument(); + + expect(() => ReactTestUtils.renderIntoDocument()).toWarnDev( + 'Warning: ComponentB defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'Did you accidentally pass the Context.Provider instead?', + {withoutStack: true}, + ); + }); + + it('should warn if you define contextType on a functional component', () => { + const Context = React.createContext(); + + function ComponentA() { + return
; + } + ComponentA.contextType = Context; + + function ComponentB() { + return
; + } + ComponentB.contextType = Context; + + expect(() => ReactTestUtils.renderIntoDocument()).toWarnDev( + 'Warning: ComponentA: Stateless functional components do not support contextType.', + {withoutStack: true}, + ); + + // Warnings should be deduped by component type + ReactTestUtils.renderIntoDocument(); + + expect(() => ReactTestUtils.renderIntoDocument()).toWarnDev( + 'Warning: ComponentB: Stateless functional components do not support contextType.', + {withoutStack: true}, + ); + }); }); diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js index bb06912f895eb..3e4dbc040dea5 100644 --- a/packages/react/src/__tests__/ReactES6Class-test.js +++ b/packages/react/src/__tests__/ReactES6Class-test.js @@ -435,6 +435,7 @@ describe('ReactES6Class', () => { constructor() { super(); this.contextTypes = {}; + this.contextType = {}; this.propTypes = {}; } getInitialState() { @@ -455,6 +456,7 @@ describe('ReactES6Class', () => { 'getInitialState was defined on Foo, a plain JavaScript class.', 'getDefaultProps was defined on Foo, a plain JavaScript class.', 'propTypes was defined as an instance property on Foo.', + 'contextType was defined as an instance property on Foo.', 'contextTypes was defined as an instance property on Foo.', ], {withoutStack: true}, diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts index d037c73dd8f4f..c738c1742bd5f 100644 --- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts +++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts @@ -239,6 +239,7 @@ let getInitialStateWasCalled = false; let getDefaultPropsWasCalled = false; class ClassicProperties extends React.Component { contextTypes = {}; + contextType = {}; propTypes = {}; getDefaultProps() { getDefaultPropsWasCalled = true; @@ -594,6 +595,7 @@ describe('ReactTypeScriptClass', function() { 'a plain JavaScript class.', 'propTypes was defined as an instance property on ClassicProperties.', 'contextTypes was defined as an instance property on ClassicProperties.', + 'contextType was defined as an instance property on ClassicProperties.', ], {withoutStack: true}); expect(getInitialStateWasCalled).toBe(false); expect(getDefaultPropsWasCalled).toBe(false);