From 0c1ec049f8832d1c27f876844666fda393036800 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 2 Aug 2019 01:21:32 +0100 Subject: [PATCH] Add a feature flag to disable legacy context (#16269) * Add a feature flag to disable legacy context * Address review - invariant -> warning - Make this.context and context argument actually undefined * Increase test coverage for lifecycles * Also disable it on the server is flag is on * Make this.context {} when disabled, but function context is undefined * Move checks inside --- ...tionLegacyContextDisabled-test.internal.js | 168 +++++++ ...eactLegacyContextDisabled-test.internal.js | 207 +++++++++ .../src/server/ReactPartialRenderer.js | 58 ++- .../src/server/ReactPartialRendererContext.js | 133 ++++-- .../src/ReactFiberBeginWork.js | 28 +- .../src/ReactFiberClassComponent.js | 67 ++- .../react-reconciler/src/ReactFiberContext.js | 419 ++++++++++-------- packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.persistent.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + 14 files changed, 810 insertions(+), 278 deletions(-) create mode 100644 packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js create mode 100644 packages/react-dom/src/__tests__/ReactLegacyContextDisabled-test.internal.js diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js new file mode 100644 index 0000000000000..e8aec9ddbacbc --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js @@ -0,0 +1,168 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); + +let React; +let ReactDOM; +let ReactFeatureFlags; +let ReactDOMServer; +let ReactTestUtils; + +function initModules() { + // Reset warning cache. + jest.resetModuleRegistry(); + React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMServer = require('react-dom/server'); + ReactTestUtils = require('react-dom/test-utils'); + + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.disableLegacyContext = true; + + // Make them available to the helpers. + return { + ReactDOM, + ReactDOMServer, + ReactTestUtils, + }; +} + +const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); + +function formatValue(val) { + if (val === null) { + return 'null'; + } + if (val === undefined) { + return 'undefined'; + } + if (typeof val === 'string') { + return val; + } + return JSON.stringify(val); +} + +describe('ReactDOMServerIntegrationLegacyContextDisabled', () => { + beforeEach(() => { + resetModules(); + }); + + itRenders('undefined legacy context with warning', async render => { + class LegacyProvider extends React.Component { + static childContextTypes = { + foo() {}, + }; + getChildContext() { + return {foo: 10}; + } + render() { + return this.props.children; + } + } + + let lifecycleContextLog = []; + class LegacyClsConsumer extends React.Component { + static contextTypes = { + foo() {}, + }; + shouldComponentUpdate(nextProps, nextState, nextContext) { + lifecycleContextLog.push(nextContext); + return true; + } + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + lifecycleContextLog.push(nextContext); + } + UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) { + lifecycleContextLog.push(nextContext); + } + render() { + return formatValue(this.context); + } + } + + function LegacyFnConsumer(props, context) { + return formatValue(context); + } + LegacyFnConsumer.contextTypes = {foo() {}}; + + function RegularFn(props, context) { + return formatValue(context); + } + + const e = await render( + + + + + + + , + 3, + ); + expect(e.textContent).toBe('{}undefinedundefined'); + expect(lifecycleContextLog).toEqual([]); + }); + + itRenders('modern context', async render => { + let Ctx = React.createContext(); + + class Provider extends React.Component { + render() { + return ( + + {this.props.children} + + ); + } + } + + class RenderPropConsumer extends React.Component { + render() { + return {value => formatValue(value)}; + } + } + + let lifecycleContextLog = []; + class ContextTypeConsumer extends React.Component { + static contextType = Ctx; + shouldComponentUpdate(nextProps, nextState, nextContext) { + lifecycleContextLog.push(nextContext); + return true; + } + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + lifecycleContextLog.push(nextContext); + } + UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) { + lifecycleContextLog.push(nextContext); + } + render() { + return formatValue(this.context); + } + } + + function FnConsumer() { + return formatValue(React.useContext(Ctx)); + } + + const e = await render( + + + + + + + , + ); + expect(e.textContent).toBe('aaa'); + expect(lifecycleContextLog).toEqual([]); + }); +}); diff --git a/packages/react-dom/src/__tests__/ReactLegacyContextDisabled-test.internal.js b/packages/react-dom/src/__tests__/ReactLegacyContextDisabled-test.internal.js new file mode 100644 index 0000000000000..f788748aa909d --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactLegacyContextDisabled-test.internal.js @@ -0,0 +1,207 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactDOM; +let ReactFeatureFlags; + +describe('ReactLegacyContextDisabled', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactDOM = require('react-dom'); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.disableLegacyContext = true; + }); + + function formatValue(val) { + if (val === null) { + return 'null'; + } + if (val === undefined) { + return 'undefined'; + } + if (typeof val === 'string') { + return val; + } + return JSON.stringify(val); + } + + it('warns for legacy context', () => { + class LegacyProvider extends React.Component { + static childContextTypes = { + foo() {}, + }; + getChildContext() { + return {foo: 10}; + } + render() { + return this.props.children; + } + } + + let lifecycleContextLog = []; + class LegacyClsConsumer extends React.Component { + static contextTypes = { + foo() {}, + }; + shouldComponentUpdate(nextProps, nextState, nextContext) { + lifecycleContextLog.push(nextContext); + return true; + } + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + lifecycleContextLog.push(nextContext); + } + UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) { + lifecycleContextLog.push(nextContext); + } + render() { + return formatValue(this.context); + } + } + + function LegacyFnConsumer(props, context) { + return formatValue(context); + } + LegacyFnConsumer.contextTypes = {foo() {}}; + + function RegularFn(props, context) { + return formatValue(context); + } + + const container = document.createElement('div'); + expect(() => { + ReactDOM.render( + + + + + + + , + container, + ); + }).toWarnDev( + [ + 'LegacyProvider uses the legacy childContextTypes API which is no longer supported. ' + + 'Use React.createContext() instead.', + 'LegacyClsConsumer uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with static contextType instead.', + 'LegacyFnConsumer uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with React.useContext() instead.', + ], + {withoutStack: true}, + ); + expect(container.textContent).toBe('{}undefinedundefined'); + expect(lifecycleContextLog).toEqual([]); + + // Test update path. + ReactDOM.render( + + + + + + + , + container, + ); + expect(container.textContent).toBe('{}undefinedundefined'); + expect(lifecycleContextLog).toEqual([{}, {}, {}]); + ReactDOM.unmountComponentAtNode(container); + }); + + it('renders a tree with modern context', () => { + let Ctx = React.createContext(); + + class Provider extends React.Component { + render() { + return ( + + {this.props.children} + + ); + } + } + + class RenderPropConsumer extends React.Component { + render() { + return {value => formatValue(value)}; + } + } + + let lifecycleContextLog = []; + class ContextTypeConsumer extends React.Component { + static contextType = Ctx; + shouldComponentUpdate(nextProps, nextState, nextContext) { + lifecycleContextLog.push(nextContext); + return true; + } + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + lifecycleContextLog.push(nextContext); + } + UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) { + lifecycleContextLog.push(nextContext); + } + render() { + return formatValue(this.context); + } + } + + function FnConsumer() { + return formatValue(React.useContext(Ctx)); + } + + const container = document.createElement('div'); + ReactDOM.render( + + + + + + + , + container, + ); + expect(container.textContent).toBe('aaa'); + expect(lifecycleContextLog).toEqual([]); + + // Test update path + ReactDOM.render( + + + + + + + , + container, + ); + expect(container.textContent).toBe('aaa'); + expect(lifecycleContextLog).toEqual(['a', 'a', 'a']); + lifecycleContextLog.length = 0; + + ReactDOM.render( + + + + + + + , + container, + ); + expect(container.textContent).toBe('bbb'); + expect(lifecycleContextLog).toEqual(['b', 'b']); // sCU skipped due to changed context value. + ReactDOM.unmountComponentAtNode(container); + }); +}); diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 718108abbbf94..08c76c72aad7d 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -21,6 +21,7 @@ import describeComponentFrame from 'shared/describeComponentFrame'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { warnAboutDeprecatedLifecycles, + disableLegacyContext, enableSuspenseServerRenderer, enableFundamentalAPI, enableFlareAPI, @@ -430,7 +431,8 @@ function resolve( // Extra closure so queue and replace can be captured properly function processChild(element, Component) { - let publicContext = processContext(Component, context, threadID); + const isClass = shouldConstruct(Component); + const publicContext = processContext(Component, context, threadID, isClass); let queue = []; let replace = false; @@ -458,7 +460,7 @@ function resolve( }; let inst; - if (shouldConstruct(Component)) { + if (isClass) { inst = new Component(element.props, publicContext, updater); if (typeof Component.getDerivedStateFromProps === 'function') { @@ -650,29 +652,43 @@ function resolve( validateRenderResult(child, Component); let childContext; - if (typeof inst.getChildContext === 'function') { - let childContextTypes = Component.childContextTypes; - if (typeof childContextTypes === 'object') { - childContext = inst.getChildContext(); - for (let contextKey in childContext) { - invariant( - contextKey in childContextTypes, - '%s.getChildContext(): key "%s" is not defined in childContextTypes.', + if (disableLegacyContext) { + if (__DEV__) { + let childContextTypes = Component.childContextTypes; + if (childContextTypes !== undefined) { + warningWithoutStack( + false, + '%s uses the legacy childContextTypes API which is no longer supported. ' + + 'Use React.createContext() instead.', getComponentName(Component) || 'Unknown', - contextKey, ); } - } else { - warningWithoutStack( - false, - '%s.getChildContext(): childContextTypes must be defined in order to ' + - 'use getChildContext().', - getComponentName(Component) || 'Unknown', - ); } - } - if (childContext) { - context = Object.assign({}, context, childContext); + } else { + if (typeof inst.getChildContext === 'function') { + let childContextTypes = Component.childContextTypes; + if (typeof childContextTypes === 'object') { + childContext = inst.getChildContext(); + for (let contextKey in childContext) { + invariant( + contextKey in childContextTypes, + '%s.getChildContext(): key "%s" is not defined in childContextTypes.', + getComponentName(Component) || 'Unknown', + contextKey, + ); + } + } else { + warningWithoutStack( + false, + '%s.getChildContext(): childContextTypes must be defined in order to ' + + 'use getChildContext().', + getComponentName(Component) || 'Unknown', + ); + } + } + if (childContext) { + context = Object.assign({}, context, childContext); + } } } return {child, context}; diff --git a/packages/react-dom/src/server/ReactPartialRendererContext.js b/packages/react-dom/src/server/ReactPartialRendererContext.js index 2ad914eb2b1ad..0caeed21251fb 100644 --- a/packages/react-dom/src/server/ReactPartialRendererContext.js +++ b/packages/react-dom/src/server/ReactPartialRendererContext.js @@ -10,6 +10,7 @@ import type {ThreadID} from './ReactThreadIDAllocator'; import type {ReactContext} from 'shared/ReactTypes'; +import {disableLegacyContext} from 'shared/ReactFeatureFlags'; import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentName from 'shared/getComponentName'; @@ -73,60 +74,100 @@ export function processContext( type: Function, context: Object, threadID: ThreadID, + isClass: boolean, ) { - const contextType = type.contextType; - if (__DEV__) { - if ('contextType' in (type: any)) { - let isValid = - // Allow null for conditional declaration - contextType === null || - (contextType !== undefined && - contextType.$$typeof === REACT_CONTEXT_TYPE && - contextType._context === undefined); // Not a + if (isClass) { + const contextType = type.contextType; + if (__DEV__) { + if ('contextType' in (type: any)) { + let isValid = + // Allow null for conditional declaration + contextType === null || + (contextType !== undefined && + contextType.$$typeof === REACT_CONTEXT_TYPE && + contextType._context === undefined); // Not a - if (!isValid && !didWarnAboutInvalidateContextType.has(type)) { - didWarnAboutInvalidateContextType.add(type); + if (!isValid && !didWarnAboutInvalidateContextType.has(type)) { + didWarnAboutInvalidateContextType.add(type); - let addendum = ''; - if (contextType === undefined) { - addendum = - ' However, it is set to undefined. ' + - 'This can be caused by a typo or by mixing up named and default imports. ' + - 'This can also happen due to a circular dependency, so ' + - 'try moving the createContext() call to a separate file.'; - } else if (typeof contextType !== 'object') { - addendum = ' However, it is set to a ' + typeof contextType + '.'; - } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) { - addendum = ' Did you accidentally pass the Context.Provider instead?'; - } else if (contextType._context !== undefined) { - // - addendum = ' Did you accidentally pass the Context.Consumer instead?'; - } else { - addendum = - ' However, it is set to an object with keys {' + - Object.keys(contextType).join(', ') + - '}.'; + let addendum = ''; + if (contextType === undefined) { + addendum = + ' However, it is set to undefined. ' + + 'This can be caused by a typo or by mixing up named and default imports. ' + + 'This can also happen due to a circular dependency, so ' + + 'try moving the createContext() call to a separate file.'; + } else if (typeof contextType !== 'object') { + addendum = ' However, it is set to a ' + typeof contextType + '.'; + } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) { + addendum = + ' Did you accidentally pass the Context.Provider instead?'; + } else if (contextType._context !== undefined) { + // + addendum = + ' Did you accidentally pass the Context.Consumer instead?'; + } else { + addendum = + ' However, it is set to an object with keys {' + + Object.keys(contextType).join(', ') + + '}.'; + } + warningWithoutStack( + false, + '%s defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext().%s', + getComponentName(type) || 'Component', + addendum, + ); } - warningWithoutStack( - false, - '%s defines an invalid contextType. ' + - 'contextType should point to the Context object returned by React.createContext().%s', - getComponentName(type) || 'Component', - addendum, - ); } } - } - if (typeof contextType === 'object' && contextType !== null) { - validateContextBounds(contextType, threadID); - return contextType[threadID]; + if (typeof contextType === 'object' && contextType !== null) { + validateContextBounds(contextType, threadID); + return contextType[threadID]; + } + if (disableLegacyContext) { + if (__DEV__) { + if (type.contextTypes) { + warningWithoutStack( + false, + '%s uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with static contextType instead.', + getComponentName(type) || 'Unknown', + ); + } + } + return emptyObject; + } else { + const maskedContext = maskContext(type, context); + if (__DEV__) { + if (type.contextTypes) { + checkContextTypes(type.contextTypes, maskedContext, 'context'); + } + } + return maskedContext; + } } else { - const maskedContext = maskContext(type, context); - if (__DEV__) { - if (type.contextTypes) { - checkContextTypes(type.contextTypes, maskedContext, 'context'); + if (disableLegacyContext) { + if (__DEV__) { + if (type.contextTypes) { + warningWithoutStack( + false, + '%s uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with React.useContext() instead.', + getComponentName(type) || 'Unknown', + ); + } + } + return undefined; + } else { + const maskedContext = maskContext(type, context); + if (__DEV__) { + if (type.contextTypes) { + checkContextTypes(type.contextTypes, maskedContext, 'context'); + } } + return maskedContext; } - return maskedContext; } } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index c3b0930a62ccd..83b7f08538b7f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -57,6 +57,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import { debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, + disableLegacyContext, enableProfilerTimer, enableSchedulerTracing, enableSuspenseServerRenderer, @@ -615,8 +616,11 @@ function updateFunctionComponent( } } - const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); - const context = getMaskedContext(workInProgress, unmaskedContext); + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); + context = getMaskedContext(workInProgress, unmaskedContext); + } let nextChildren; prepareToReadContext(workInProgress, renderExpirationTime); @@ -1230,8 +1234,15 @@ function mountIndeterminateComponent( } const props = workInProgress.pendingProps; - const unmaskedContext = getUnmaskedContext(workInProgress, Component, false); - const context = getMaskedContext(workInProgress, unmaskedContext); + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext( + workInProgress, + Component, + false, + ); + context = getMaskedContext(workInProgress, unmaskedContext); + } prepareToReadContext(workInProgress, renderExpirationTime); let value; @@ -1349,6 +1360,15 @@ function mountIndeterminateComponent( // Proceed under the assumption that this is a function component workInProgress.tag = FunctionComponent; if (__DEV__) { + if (disableLegacyContext && Component.contextTypes) { + warningWithoutStack( + false, + '%s uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with React.useContext() instead.', + getComponentName(Component) || 'Unknown', + ); + } + if ( debugRenderPhaseSideEffects || (debugRenderPhaseSideEffectsForStrictMode && diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index fcadf0a22ce80..7ccc16023eaec 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -15,6 +15,7 @@ import {Update, Snapshot} from 'shared/ReactSideEffectTags'; import { debugRenderPhaseSideEffects, debugRenderPhaseSideEffectsForStrictMode, + disableLegacyContext, warnAboutDeprecatedLifecycles, } from 'shared/ReactFeatureFlags'; import ReactStrictModeWarnings from './ReactStrictModeWarnings'; @@ -361,26 +362,46 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { 'property to define contextType instead.', name, ); - const noInstanceContextTypes = !instance.contextTypes; - warningWithoutStack( - noInstanceContextTypes, - 'contextTypes was defined as an instance property on %s. Use a static ' + - 'property to define contextTypes instead.', - name, - ); - if ( - ctor.contextType && - ctor.contextTypes && - !didWarnAboutContextTypeAndContextTypes.has(ctor) - ) { - didWarnAboutContextTypeAndContextTypes.add(ctor); + if (disableLegacyContext) { + if (ctor.childContextTypes) { + warningWithoutStack( + false, + '%s uses the legacy childContextTypes API which is no longer supported. ' + + 'Use React.createContext() instead.', + name, + ); + } + if (ctor.contextTypes) { + warningWithoutStack( + false, + '%s uses the legacy contextTypes API which is no longer supported. ' + + 'Use React.createContext() with static contextType instead.', + name, + ); + } + } else { + const noInstanceContextTypes = !instance.contextTypes; warningWithoutStack( - false, - '%s declares both contextTypes and contextType static properties. ' + - 'The legacy contextTypes property will be ignored.', + noInstanceContextTypes, + 'contextTypes was defined as an instance property on %s. Use a static ' + + '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, + ); + } } const noComponentShouldUpdate = @@ -534,7 +555,7 @@ function constructClassInstance( ): any { let isLegacyContextConsumer = false; let unmaskedContext = emptyContextObject; - let context = null; + let context = emptyContextObject; const contextType = ctor.contextType; if (__DEV__) { @@ -582,7 +603,7 @@ function constructClassInstance( if (typeof contextType === 'object' && contextType !== null) { context = readContext((contextType: any)); - } else { + } else if (!disableLegacyContext) { unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); const contextTypes = ctor.contextTypes; isLegacyContextConsumer = @@ -785,6 +806,8 @@ function mountClassInstance( const contextType = ctor.contextType; if (typeof contextType === 'object' && contextType !== null) { instance.context = readContext(contextType); + } else if (disableLegacyContext) { + instance.context = emptyContextObject; } else { const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); instance.context = getMaskedContext(workInProgress, unmaskedContext); @@ -885,10 +908,10 @@ function resumeMountClassInstance( const oldContext = instance.context; const contextType = ctor.contextType; - let nextContext; + let nextContext = emptyContextObject; if (typeof contextType === 'object' && contextType !== null) { nextContext = readContext(contextType); - } else { + } else if (!disableLegacyContext) { const nextLegacyUnmaskedContext = getUnmaskedContext( workInProgress, ctor, @@ -1034,10 +1057,10 @@ function updateClassInstance( const oldContext = instance.context; const contextType = ctor.contextType; - let nextContext; + let nextContext = emptyContextObject; if (typeof contextType === 'object' && contextType !== null) { nextContext = readContext(contextType); - } else { + } else if (!disableLegacyContext) { const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true); nextContext = getMaskedContext(workInProgress, nextUnmaskedContext); } diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 74826408ddf4a..7fd763bd5ca11 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -11,6 +11,7 @@ import type {Fiber} from './ReactFiber'; import type {StackCursor} from './ReactFiberStack'; import {isFiberMounted} from 'react-reconciler/reflection'; +import {disableLegacyContext} from 'shared/ReactFeatureFlags'; import {ClassComponent, HostRoot} from 'shared/ReactWorkTags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; @@ -46,14 +47,18 @@ function getUnmaskedContext( Component: Function, didPushOwnContextIfProvider: boolean, ): Object { - if (didPushOwnContextIfProvider && isContextProvider(Component)) { - // If the fiber is a context provider itself, when we read its context - // we may have already pushed its own child context on the stack. A context - // provider should not "see" its own child context. Therefore we read the - // previous (parent) context instead for a context provider. - return previousContext; + if (disableLegacyContext) { + return emptyContextObject; + } else { + if (didPushOwnContextIfProvider && isContextProvider(Component)) { + // If the fiber is a context provider itself, when we read its context + // we may have already pushed its own child context on the stack. A context + // provider should not "see" its own child context. Therefore we read the + // previous (parent) context instead for a context provider. + return previousContext; + } + return contextStackCursor.current; } - return contextStackCursor.current; } function cacheContext( @@ -61,74 +66,98 @@ function cacheContext( unmaskedContext: Object, maskedContext: Object, ): void { - const instance = workInProgress.stateNode; - instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; - instance.__reactInternalMemoizedMaskedChildContext = maskedContext; + if (disableLegacyContext) { + return; + } else { + const instance = workInProgress.stateNode; + instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; + instance.__reactInternalMemoizedMaskedChildContext = maskedContext; + } } function getMaskedContext( workInProgress: Fiber, unmaskedContext: Object, ): Object { - const type = workInProgress.type; - const contextTypes = type.contextTypes; - if (!contextTypes) { + if (disableLegacyContext) { return emptyContextObject; - } + } else { + const type = workInProgress.type; + const contextTypes = type.contextTypes; + if (!contextTypes) { + return emptyContextObject; + } - // Avoid recreating masked context unless unmasked context has changed. - // Failing to do this will result in unnecessary calls to componentWillReceiveProps. - // This may trigger infinite loops if componentWillReceiveProps calls setState. - const instance = workInProgress.stateNode; - if ( - instance && - instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext - ) { - return instance.__reactInternalMemoizedMaskedChildContext; - } + // Avoid recreating masked context unless unmasked context has changed. + // Failing to do this will result in unnecessary calls to componentWillReceiveProps. + // This may trigger infinite loops if componentWillReceiveProps calls setState. + const instance = workInProgress.stateNode; + if ( + instance && + instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext + ) { + return instance.__reactInternalMemoizedMaskedChildContext; + } - const context = {}; - for (let key in contextTypes) { - context[key] = unmaskedContext[key]; - } + const context = {}; + for (let key in contextTypes) { + context[key] = unmaskedContext[key]; + } - if (__DEV__) { - const name = getComponentName(type) || 'Unknown'; - checkPropTypes( - contextTypes, - context, - 'context', - name, - getCurrentFiberStackInDev, - ); - } + if (__DEV__) { + const name = getComponentName(type) || 'Unknown'; + checkPropTypes( + contextTypes, + context, + 'context', + name, + getCurrentFiberStackInDev, + ); + } - // Cache unmasked context so we can avoid recreating masked context unless necessary. - // Context is created before the class component is instantiated so check for instance. - if (instance) { - cacheContext(workInProgress, unmaskedContext, context); - } + // Cache unmasked context so we can avoid recreating masked context unless necessary. + // Context is created before the class component is instantiated so check for instance. + if (instance) { + cacheContext(workInProgress, unmaskedContext, context); + } - return context; + return context; + } } function hasContextChanged(): boolean { - return didPerformWorkStackCursor.current; + if (disableLegacyContext) { + return false; + } else { + return didPerformWorkStackCursor.current; + } } function isContextProvider(type: Function): boolean { - const childContextTypes = type.childContextTypes; - return childContextTypes !== null && childContextTypes !== undefined; + if (disableLegacyContext) { + return false; + } else { + const childContextTypes = type.childContextTypes; + return childContextTypes !== null && childContextTypes !== undefined; + } } function popContext(fiber: Fiber): void { - pop(didPerformWorkStackCursor, fiber); - pop(contextStackCursor, fiber); + if (disableLegacyContext) { + return; + } else { + pop(didPerformWorkStackCursor, fiber); + pop(contextStackCursor, fiber); + } } function popTopLevelContextObject(fiber: Fiber): void { - pop(didPerformWorkStackCursor, fiber); - pop(contextStackCursor, fiber); + if (disableLegacyContext) { + return; + } else { + pop(didPerformWorkStackCursor, fiber); + pop(contextStackCursor, fiber); + } } function pushTopLevelContextObject( @@ -136,14 +165,18 @@ function pushTopLevelContextObject( context: Object, didChange: boolean, ): void { - invariant( - contextStackCursor.current === emptyContextObject, - 'Unexpected context found on stack. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - - push(contextStackCursor, context, fiber); - push(didPerformWorkStackCursor, didChange, fiber); + if (disableLegacyContext) { + return; + } else { + invariant( + contextStackCursor.current === emptyContextObject, + 'Unexpected context found on stack. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + push(contextStackCursor, context, fiber); + push(didPerformWorkStackCursor, didChange, fiber); + } } function processChildContext( @@ -151,87 +184,95 @@ function processChildContext( type: any, parentContext: Object, ): Object { - const instance = fiber.stateNode; - const childContextTypes = type.childContextTypes; + if (disableLegacyContext) { + return parentContext; + } else { + const instance = fiber.stateNode; + const childContextTypes = type.childContextTypes; + + // TODO (bvaughn) Replace this behavior with an invariant() in the future. + // It has only been added in Fiber to match the (unintentional) behavior in Stack. + if (typeof instance.getChildContext !== 'function') { + if (__DEV__) { + const componentName = getComponentName(type) || 'Unknown'; + + if (!warnedAboutMissingGetChildContext[componentName]) { + warnedAboutMissingGetChildContext[componentName] = true; + warningWithoutStack( + false, + '%s.childContextTypes is specified but there is no getChildContext() method ' + + 'on the instance. You can either define getChildContext() on %s or remove ' + + 'childContextTypes from it.', + componentName, + componentName, + ); + } + } + return parentContext; + } - // TODO (bvaughn) Replace this behavior with an invariant() in the future. - // It has only been added in Fiber to match the (unintentional) behavior in Stack. - if (typeof instance.getChildContext !== 'function') { + let childContext; if (__DEV__) { - const componentName = getComponentName(type) || 'Unknown'; - - if (!warnedAboutMissingGetChildContext[componentName]) { - warnedAboutMissingGetChildContext[componentName] = true; - warningWithoutStack( - false, - '%s.childContextTypes is specified but there is no getChildContext() method ' + - 'on the instance. You can either define getChildContext() on %s or remove ' + - 'childContextTypes from it.', - componentName, - componentName, - ); - } + setCurrentPhase('getChildContext'); + } + startPhaseTimer(fiber, 'getChildContext'); + childContext = instance.getChildContext(); + stopPhaseTimer(); + if (__DEV__) { + setCurrentPhase(null); + } + for (let contextKey in childContext) { + invariant( + contextKey in childContextTypes, + '%s.getChildContext(): key "%s" is not defined in childContextTypes.', + getComponentName(type) || 'Unknown', + contextKey, + ); + } + if (__DEV__) { + const name = getComponentName(type) || 'Unknown'; + checkPropTypes( + childContextTypes, + childContext, + 'child context', + name, + // In practice, there is one case in which we won't get a stack. It's when + // somebody calls unstable_renderSubtreeIntoContainer() and we process + // context from the parent component instance. The stack will be missing + // because it's outside of the reconciliation, and so the pointer has not + // been set. This is rare and doesn't matter. We'll also remove that API. + getCurrentFiberStackInDev, + ); } - return parentContext; - } - let childContext; - if (__DEV__) { - setCurrentPhase('getChildContext'); + return {...parentContext, ...childContext}; } - startPhaseTimer(fiber, 'getChildContext'); - childContext = instance.getChildContext(); - stopPhaseTimer(); - if (__DEV__) { - setCurrentPhase(null); - } - for (let contextKey in childContext) { - invariant( - contextKey in childContextTypes, - '%s.getChildContext(): key "%s" is not defined in childContextTypes.', - getComponentName(type) || 'Unknown', - contextKey, - ); - } - if (__DEV__) { - const name = getComponentName(type) || 'Unknown'; - checkPropTypes( - childContextTypes, - childContext, - 'child context', - name, - // In practice, there is one case in which we won't get a stack. It's when - // somebody calls unstable_renderSubtreeIntoContainer() and we process - // context from the parent component instance. The stack will be missing - // because it's outside of the reconciliation, and so the pointer has not - // been set. This is rare and doesn't matter. We'll also remove that API. - getCurrentFiberStackInDev, - ); - } - - return {...parentContext, ...childContext}; } function pushContextProvider(workInProgress: Fiber): boolean { - const instance = workInProgress.stateNode; - // We push the context as early as possible to ensure stack integrity. - // If the instance does not exist yet, we will push null at first, - // and replace it on the stack later when invalidating the context. - const memoizedMergedChildContext = - (instance && instance.__reactInternalMemoizedMergedChildContext) || - emptyContextObject; - - // Remember the parent context so we can merge with it later. - // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates. - previousContext = contextStackCursor.current; - push(contextStackCursor, memoizedMergedChildContext, workInProgress); - push( - didPerformWorkStackCursor, - didPerformWorkStackCursor.current, - workInProgress, - ); - - return true; + if (disableLegacyContext) { + return false; + } else { + const instance = workInProgress.stateNode; + // We push the context as early as possible to ensure stack integrity. + // If the instance does not exist yet, we will push null at first, + // and replace it on the stack later when invalidating the context. + const memoizedMergedChildContext = + (instance && instance.__reactInternalMemoizedMergedChildContext) || + emptyContextObject; + + // Remember the parent context so we can merge with it later. + // Inherit the parent's did-perform-work value to avoid inadvertently blocking updates. + previousContext = contextStackCursor.current; + push(contextStackCursor, memoizedMergedChildContext, workInProgress); + push( + didPerformWorkStackCursor, + didPerformWorkStackCursor.current, + workInProgress, + ); + + return true; + } } function invalidateContextProvider( @@ -239,66 +280,74 @@ function invalidateContextProvider( type: any, didChange: boolean, ): void { - const instance = workInProgress.stateNode; - invariant( - instance, - 'Expected to have an instance by this point. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - - if (didChange) { - // Merge parent and own context. - // Skip this if we're not updating due to sCU. - // This avoids unnecessarily recomputing memoized values. - const mergedContext = processChildContext( - workInProgress, - type, - previousContext, - ); - instance.__reactInternalMemoizedMergedChildContext = mergedContext; - - // Replace the old (or empty) context with the new one. - // It is important to unwind the context in the reverse order. - pop(didPerformWorkStackCursor, workInProgress); - pop(contextStackCursor, workInProgress); - // Now push the new context and mark that it has changed. - push(contextStackCursor, mergedContext, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); + if (disableLegacyContext) { + return; } else { - pop(didPerformWorkStackCursor, workInProgress); - push(didPerformWorkStackCursor, didChange, workInProgress); + const instance = workInProgress.stateNode; + invariant( + instance, + 'Expected to have an instance by this point. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + if (didChange) { + // Merge parent and own context. + // Skip this if we're not updating due to sCU. + // This avoids unnecessarily recomputing memoized values. + const mergedContext = processChildContext( + workInProgress, + type, + previousContext, + ); + instance.__reactInternalMemoizedMergedChildContext = mergedContext; + + // Replace the old (or empty) context with the new one. + // It is important to unwind the context in the reverse order. + pop(didPerformWorkStackCursor, workInProgress); + pop(contextStackCursor, workInProgress); + // Now push the new context and mark that it has changed. + push(contextStackCursor, mergedContext, workInProgress); + push(didPerformWorkStackCursor, didChange, workInProgress); + } else { + pop(didPerformWorkStackCursor, workInProgress); + push(didPerformWorkStackCursor, didChange, workInProgress); + } } } function findCurrentUnmaskedContext(fiber: Fiber): Object { - // Currently this is only used with renderSubtreeIntoContainer; not sure if it - // makes sense elsewhere - invariant( - isFiberMounted(fiber) && fiber.tag === ClassComponent, - 'Expected subtree parent to be a mounted class component. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - - let node = fiber; - do { - switch (node.tag) { - case HostRoot: - return node.stateNode.context; - case ClassComponent: { - const Component = node.type; - if (isContextProvider(Component)) { - return node.stateNode.__reactInternalMemoizedMergedChildContext; + if (disableLegacyContext) { + return emptyContextObject; + } else { + // Currently this is only used with renderSubtreeIntoContainer; not sure if it + // makes sense elsewhere + invariant( + isFiberMounted(fiber) && fiber.tag === ClassComponent, + 'Expected subtree parent to be a mounted class component. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + + let node = fiber; + do { + switch (node.tag) { + case HostRoot: + return node.stateNode.context; + case ClassComponent: { + const Component = node.type; + if (isContextProvider(Component)) { + return node.stateNode.__reactInternalMemoizedMergedChildContext; + } + break; } - break; } - } - node = node.return; - } while (node !== null); - invariant( - false, - 'Found unexpected detached subtree parent. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); + node = node.return; + } while (node !== null); + invariant( + false, + 'Found unexpected detached subtree parent. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } } export { diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 6dc443b3e7ed8..da01edb138783 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -92,3 +92,5 @@ export const enableSuspenseCallback = false; // from React.createElement to React.jsx // https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md export const warnAboutDefaultPropsOnFunctionComponents = false; + +export const disableLegacyContext = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 7c6783463aa59..6b95b92d207ef 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -40,6 +40,7 @@ export const flushSuspenseFallbacksInTests = true; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; +export const disableLegacyContext = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 291e5d9c58b8f..08525d0c0938b 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -35,6 +35,7 @@ export const flushSuspenseFallbacksInTests = true; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; +export const disableLegacyContext = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index e62a6f88dc768..478586ff80a04 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -35,6 +35,7 @@ export const flushSuspenseFallbacksInTests = true; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; +export const disableLegacyContext = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 8b8013db7bd33..91bfab22efa0e 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -35,6 +35,7 @@ export const flushSuspenseFallbacksInTests = true; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; +export const disableLegacyContext = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 813691c9dfb14..28f7d534292f4 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -35,6 +35,7 @@ export const flushSuspenseFallbacksInTests = true; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = true; export const warnAboutDefaultPropsOnFunctionComponents = false; +export const disableLegacyContext = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index e176657cad280..3f2c4c6b519be 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -21,6 +21,7 @@ export const { warnAboutDeprecatedSetNativeProps, revertPassiveEffectsChange, enableUserBlockingEvents, + disableLegacyContext, } = require('ReactFeatureFlags'); // In www, we have experimental support for gathering data