Skip to content

Commit

Permalink
SSR support for class contextType (facebook#13889)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage authored and jetoneza committed Jan 23, 2019
1 parent a9b19ee commit 3795161
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<Context.Provider value={this.props.text}>
{this.props.children}
</Context.Provider>
);
}
}
PurpleContext = props => <Parent text="purple">{props.children}</Parent>;
RedContext = props => <Parent text="red">{props.children}</Parent>;
});

itRenders('class child with context', async render => {
class ClassChildWithContext extends React.Component {
static contextType = Context;
render() {
const text = this.context;
return <div>{text}</div>;
}
}

const e = await render(
<PurpleContext>
<ClassChildWithContext />
</PurpleContext>,
);
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 (
<div>{typeof this.context === 'string' ? this.context : ''}</div>
);
}
}

const e = await render(
<PurpleContext>
<ClassChildWithoutContext />
</PurpleContext>,
);
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 <div id="classWrongChild">{this.context.foo}</div>;
}
}

const e = await render(
<PurpleContext>
<ClassChildWithWrongContext />
</PurpleContext>,
);
expect(e.textContent).toBe('');
});

itRenders('with context passed through to a grandchild', async render => {
class Grandchild extends React.Component {
static contextType = Context;
render() {
return <div>{this.context}</div>;
}
}

const Child = props => <Grandchild />;

const e = await render(
<PurpleContext>
<Child />
</PurpleContext>,
);
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 <div>{this.context}</div>;
}
}

const e = await render(
<PurpleContext>
<RedContext>
<Grandchild />
</RedContext>
</PurpleContext>,
);
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 (
<Theme.Provider value="light">
<Child />
</Theme.Provider>
);
}
}

function Child() {
return (
<Language.Provider value="english">
<Grandchild />
</Language.Provider>
);
}

class ThemeComponent extends React.Component {
static contextType = Theme;
render() {
return <div id="theme">{this.context}</div>;
}
}

class LanguageComponent extends React.Component {
static contextType = Language;
render() {
return <div id="language">{this.context}</div>;
}
}

const Grandchild = props => {
return (
<div>
<ThemeComponent />
<LanguageComponent />
</div>
);
};

const e = await render(<Parent />);
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 = () => (
<div>
<Theme.Provider value="light">
<Language.Provider value="english">
<Theme.Provider value="dark">
<ThemeConsumer>
{theme => <div id="theme1">{theme}</div>}
</ThemeConsumer>
</Theme.Provider>
<ThemeConsumer>
{theme => <div id="theme2">{theme}</div>}
</ThemeConsumer>
<Language.Provider value="sanskrit">
<Theme.Provider value="blue">
<Theme.Provider value="red">
<LanguageConsumer>
{() => (
<Language.Provider value="chinese">
<Language.Provider value="hungarian" />
<LanguageConsumer>
{language => <div id="language1">{language}</div>}
</LanguageConsumer>
</Language.Provider>
)}
</LanguageConsumer>
</Theme.Provider>
<LanguageConsumer>
{language => (
<React.Fragment>
<ThemeConsumer>
{theme => <div id="theme3">{theme}</div>}
</ThemeConsumer>
<div id="language2">{language}</div>
</React.Fragment>
)}
</LanguageConsumer>
</Theme.Provider>
</Language.Provider>
</Language.Provider>
</Theme.Provider>
<LanguageConsumer>
{language => <div id="language3">{language}</div>}
</LanguageConsumer>
</div>
);
let e = await render(<App />);
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');
});
});
});
31 changes: 26 additions & 5 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ const didWarnAboutBadClass = {};
const didWarnAboutDeprecatedWillMount = {};
const didWarnAboutUndefinedDerivedState = {};
const didWarnAboutUninitializedState = {};
const didWarnAboutInvalidateContextType = {};
const valuePropNames = ['value', 'defaultValue'];
const newlineEatingTags = {
listing: true,
Expand Down Expand Up @@ -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) || 'Component';
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,
);
}
}
}
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;
Expand Down
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 3795161

Please sign in to comment.