diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js new file mode 100644 index 0000000000000..19839aa7cd6c2 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js @@ -0,0 +1,266 @@ +/** + * 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 ReactDOMServer; + +function initModules() { + // Reset warning cache. + jest.resetModuleRegistry(); + React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMServer = require('react-dom/server'); + + // Make them available to the helpers. + return { + ReactDOM, + ReactDOMServer, + }; +} + +const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); + +describe('ReactDOMServerIntegration', () => { + beforeEach(() => { + resetModules(); + }); + + describe('class contextType', function() { + let PurpleContext, RedContext, Context; + beforeEach(() => { + Context = React.createContext('none'); + + class Parent extends React.Component { + render() { + return ( + + {this.props.children} + + ); + } + } + PurpleContext = props => {props.children}; + RedContext = props => {props.children}; + }); + + itRenders('class child with context', async render => { + class ClassChildWithContext extends React.Component { + static contextType = Context; + render() { + const text = this.context; + return
{text}
; + } + } + + const e = await render( + + + , + ); + expect(e.textContent).toBe('purple'); + }); + + itRenders('class child without context', async render => { + class ClassChildWithoutContext extends React.Component { + render() { + // this should render blank; context isn't passed to this component. + return ( +
{typeof this.context === 'string' ? this.context : ''}
+ ); + } + } + + const e = await render( + + + , + ); + expect(e.textContent).toBe(''); + }); + + itRenders('class child with wrong context', async render => { + class ClassChildWithWrongContext extends React.Component { + static contextType = Context; + render() { + // this should render blank; context.foo isn't passed to this component. + return
{this.context.foo}
; + } + } + + const e = await render( + + + , + ); + expect(e.textContent).toBe(''); + }); + + itRenders('with context passed through to a grandchild', async render => { + class Grandchild extends React.Component { + static contextType = Context; + render() { + return
{this.context}
; + } + } + + const Child = props => ; + + const e = await render( + + + , + ); + expect(e.textContent).toBe('purple'); + }); + + itRenders('a child context overriding a parent context', async render => { + class Grandchild extends React.Component { + static contextType = Context; + render() { + return
{this.context}
; + } + } + + const e = await render( + + + + + , + ); + expect(e.textContent).toBe('red'); + }); + + itRenders('multiple contexts', async render => { + const Theme = React.createContext('dark'); + const Language = React.createContext('french'); + class Parent extends React.Component { + render() { + return ( + + + + ); + } + } + + function Child() { + return ( + + + + ); + } + + class ThemeComponent extends React.Component { + static contextType = Theme; + render() { + return
{this.context}
; + } + } + + class LanguageComponent extends React.Component { + static contextType = Language; + render() { + return
{this.context}
; + } + } + + const Grandchild = props => { + return ( +
+ + +
+ ); + }; + + const e = await render(); + expect(e.querySelector('#theme').textContent).toBe('light'); + expect(e.querySelector('#language').textContent).toBe('english'); + }); + + itRenders('nested context unwinding', async render => { + const Theme = React.createContext('dark'); + const Language = React.createContext('french'); + + class ThemeConsumer extends React.Component { + static contextType = Theme; + render() { + return this.props.children(this.context); + } + } + + class LanguageConsumer extends React.Component { + static contextType = Language; + render() { + return this.props.children(this.context); + } + } + + const App = () => ( +
+ + + + + {theme =>
{theme}
} +
+
+ + {theme =>
{theme}
} +
+ + + + + {() => ( + + + + {language =>
{language}
} +
+
+ )} +
+
+ + {language => ( + + + {theme =>
{theme}
} +
+
{language}
+
+ )} +
+
+
+
+
+ + {language =>
{language}
} +
+
+ ); + let e = await render(); + expect(e.querySelector('#theme1').textContent).toBe('dark'); + expect(e.querySelector('#theme2').textContent).toBe('light'); + expect(e.querySelector('#theme3').textContent).toBe('blue'); + expect(e.querySelector('#language1').textContent).toBe('chinese'); + expect(e.querySelector('#language2').textContent).toBe('sanskrit'); + expect(e.querySelector('#language3').textContent).toBe('french'); + }); + }); +}); diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index faa24884d169d..59d855bbaeeb9 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -180,6 +180,7 @@ const didWarnAboutBadClass = {}; const didWarnAboutDeprecatedWillMount = {}; const didWarnAboutUndefinedDerivedState = {}; const didWarnAboutUninitializedState = {}; +const didWarnAboutInvalidateContextType = {}; const valuePropNames = ['value', 'defaultValue']; const newlineEatingTags = { listing: true, @@ -357,13 +358,33 @@ function checkContextTypes(typeSpecs, values, location: string) { } function processContext(type, context) { - const maskedContext = maskContext(type, context); - if (__DEV__) { - if (type.contextTypes) { - checkContextTypes(type.contextTypes, maskedContext, 'context'); + const contextType = type.contextType; + if (typeof contextType === 'object' && contextType !== null) { + if (__DEV__) { + if (contextType.$$typeof !== REACT_CONTEXT_TYPE) { + let name = getComponentName(type); + if (!didWarnAboutInvalidateContextType[name]) { + didWarnAboutInvalidateContextType[type] = true; + 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 || 'Component', + ); + } + } } + return contextType._currentValue; + } else { + const maskedContext = maskContext(type, context); + if (__DEV__) { + if (type.contextTypes) { + checkContextTypes(type.contextTypes, maskedContext, 'context'); + } + } + return maskedContext; } - return maskedContext; } const hasOwnProperty = Object.prototype.hasOwnProperty; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index c2a87d8e9947c..57114af3e9f58 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -25,9 +25,11 @@ import shallowEqual from 'shared/shallowEqual'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; +import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {StrictMode} from './ReactTypeOfMode'; + import { enqueueUpdate, processUpdateQueue, @@ -516,7 +518,7 @@ function constructClassInstance( if (typeof contextType === 'object' && contextType !== null) { if (__DEV__) { if ( - contextType.Consumer === undefined && + contextType.$$typeof !== REACT_CONTEXT_TYPE && !didWarnAboutInvalidateContextType.has(ctor) ) { didWarnAboutInvalidateContextType.add(ctor);