diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 7a7e91ef3f01d..035193cfcdcfb 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1101,12 +1101,35 @@ function updateContextProvider( return workInProgress.child; } +let hasWarnedAboutUsingContextAsConsumer = false; + function updateContextConsumer( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ) { - const context: ReactContext = workInProgress.type; + let context: ReactContext = workInProgress.type; + // The logic below for Context differs depending on PROD or DEV mode. In + // DEV mode, we create a separate object for Context.Consumer that acts + // like a proxy to Context. This proxy object adds unnecessary code in PROD + // so we use the old behaviour (Context.Consumer references Context) to + // reduce size and overhead. The separate object references context via + // a property called "_context", which also gives us the ability to check + // in DEV mode if this property exists or not and warn if it does not. + if (__DEV__) { + if ((context: any)._context === undefined) { + if (!hasWarnedAboutUsingContextAsConsumer) { + hasWarnedAboutUsingContextAsConsumer = true; + warning( + false, + 'Rendering directly is not supported and will be removed in ' + + 'a future major release. Did you mean to render instead?', + ); + } + } else { + context = (context: any)._context; + } + } const newProps = workInProgress.pendingProps; const render = newProps.children; diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js index 9570a25e45bee..03b52dccdc863 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js @@ -1190,12 +1190,12 @@ describe('ReactNewContext', () => { function FooAndBar() { return ( - + {foo => { const bar = BarContext.unstable_read(); return ; }} - + ); } @@ -1558,4 +1558,105 @@ Context fuzz tester error! Copy and paste the following line into the test suite } }); }); + + it('should warn with an error message when using context as a consumer in DEV', () => { + const BarContext = React.createContext({value: 'bar-initial'}); + const BarConsumer = BarContext; + + function Component() { + return ( + + + + {({value}) =>
} + + + + ); + } + + expect(() => { + ReactNoop.render(); + ReactNoop.flush(); + }).toWarnDev( + 'Rendering directly is not supported and will be removed in ' + + 'a future major release. Did you mean to render instead?', + ); + }); + + it('should warn with an error message when using nested context consumers in DEV', () => { + const BarContext = React.createContext({value: 'bar-initial'}); + const BarConsumer = BarContext; + + function Component() { + return ( + + + + {({value}) =>
} + + + + ); + } + + expect(() => { + ReactNoop.render(); + ReactNoop.flush(); + }).toWarnDev( + 'Rendering is not supported and will be removed in ' + + 'a future major release. Did you mean to render instead?', + ); + }); + + it('should warn with an error message when using Context.Consumer.unstable_read() DEV', () => { + const BarContext = React.createContext({value: 'bar-initial'}); + + function Child() { + let value = BarContext.Consumer.unstable_read(); + return
; + } + + function Component() { + return ( + + + + + + ); + } + + expect(() => { + ReactNoop.render(); + ReactNoop.flush(); + }).toWarnDev( + 'Calling Context.Consumer.unstable_read() is not supported and will be removed in ' + + 'a future major release. Did you mean to render Context.unstable_read() instead?', + ); + }); + + it('should warn with an error message when using Context.Consumer.Provider DEV', () => { + const BarContext = React.createContext({value: 'bar-initial'}); + + function Component() { + return ( + + + + {({value}) =>
} + + + + ); + } + + expect(() => { + ReactNoop.render(); + ReactNoop.flush(); + }).toWarnDev( + 'Rendering is not supported and will be removed in ' + + 'a future major release. Did you mean to render instead?', + ); + }); }); diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index bf63f8dfa7d1a..fa9ff223f83cb 100644 --- a/packages/react/src/ReactContext.js +++ b/packages/react/src/ReactContext.js @@ -13,6 +13,7 @@ import type {ReactContext} from 'shared/ReactTypes'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; +import warning from 'shared/warning'; import ReactCurrentOwner from './ReactCurrentOwner'; @@ -67,9 +68,87 @@ export function createContext( $$typeof: REACT_PROVIDER_TYPE, _context: context, }; - context.Consumer = context; + context.unstable_read = readContext.bind(null, context); + let hasWarnedAboutUsingNestedContextConsumers = false; + let hasWarnedAboutUsingConsumerUnstableRead = false; + let hasWarnedAboutUsingConsumerProvider = false; + + if (__DEV__) { + // A separate object, but proxies back to the original context object for + // backwards compatibility. It has a different $$typeof, so we can properly + // warn for the incorrect usage of Context as a Consumer. + const Consumer = { + $$typeof: REACT_CONTEXT_TYPE, + _context: context, + _calculateChangedBits: context._calculateChangedBits, + unstable_read() { + if (!hasWarnedAboutUsingConsumerUnstableRead) { + hasWarnedAboutUsingConsumerUnstableRead = true; + warning( + false, + 'Calling Context.Consumer.unstable_read() is not supported and will be removed in ' + + 'a future major release. Did you mean to render Context.unstable_read() instead?', + ); + } + return context.unstable_read(); + }, + }; + // $FlowFixMe: Flow complains about not setting a value, which is intentional here + Object.defineProperties(Consumer, { + Provider: { + get() { + if (!hasWarnedAboutUsingConsumerProvider) { + hasWarnedAboutUsingConsumerProvider = true; + warning( + false, + 'Rendering is not supported and will be removed in ' + + 'a future major release. Did you mean to render instead?', + ); + } + return context.Provider; + }, + set(_Provider) { + context.Provider = _Provider; + }, + }, + _currentValue: { + get() { + return context._currentValue; + }, + set(_currentValue) { + context._currentValue = _currentValue; + }, + }, + _currentValue2: { + get() { + return context._currentValue2; + }, + set(_currentValue2) { + context._currentValue2 = _currentValue2; + }, + }, + Consumer: { + get() { + if (!hasWarnedAboutUsingNestedContextConsumers) { + hasWarnedAboutUsingNestedContextConsumers = true; + warning( + false, + 'Rendering is not supported and will be removed in ' + + 'a future major release. Did you mean to render instead?', + ); + } + return context.Consumer; + }, + }, + }); + // $FlowFixMe: Flow complains about missing properties because it doesn't understand defineProperty + context.Consumer = Consumer; + } else { + context.Consumer = context; + } + if (__DEV__) { context._currentRenderer = null; context._currentRenderer2 = null;