Skip to content

Commit 6938dca

Browse files
authored
SSR support for class contextType (#13889)
1 parent fa65c58 commit 6938dca

File tree

3 files changed

+295
-6
lines changed

3 files changed

+295
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13+
14+
let React;
15+
let ReactDOM;
16+
let ReactDOMServer;
17+
18+
function initModules() {
19+
// Reset warning cache.
20+
jest.resetModuleRegistry();
21+
React = require('react');
22+
ReactDOM = require('react-dom');
23+
ReactDOMServer = require('react-dom/server');
24+
25+
// Make them available to the helpers.
26+
return {
27+
ReactDOM,
28+
ReactDOMServer,
29+
};
30+
}
31+
32+
const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
33+
34+
describe('ReactDOMServerIntegration', () => {
35+
beforeEach(() => {
36+
resetModules();
37+
});
38+
39+
describe('class contextType', function() {
40+
let PurpleContext, RedContext, Context;
41+
beforeEach(() => {
42+
Context = React.createContext('none');
43+
44+
class Parent extends React.Component {
45+
render() {
46+
return (
47+
<Context.Provider value={this.props.text}>
48+
{this.props.children}
49+
</Context.Provider>
50+
);
51+
}
52+
}
53+
PurpleContext = props => <Parent text="purple">{props.children}</Parent>;
54+
RedContext = props => <Parent text="red">{props.children}</Parent>;
55+
});
56+
57+
itRenders('class child with context', async render => {
58+
class ClassChildWithContext extends React.Component {
59+
static contextType = Context;
60+
render() {
61+
const text = this.context;
62+
return <div>{text}</div>;
63+
}
64+
}
65+
66+
const e = await render(
67+
<PurpleContext>
68+
<ClassChildWithContext />
69+
</PurpleContext>,
70+
);
71+
expect(e.textContent).toBe('purple');
72+
});
73+
74+
itRenders('class child without context', async render => {
75+
class ClassChildWithoutContext extends React.Component {
76+
render() {
77+
// this should render blank; context isn't passed to this component.
78+
return (
79+
<div>{typeof this.context === 'string' ? this.context : ''}</div>
80+
);
81+
}
82+
}
83+
84+
const e = await render(
85+
<PurpleContext>
86+
<ClassChildWithoutContext />
87+
</PurpleContext>,
88+
);
89+
expect(e.textContent).toBe('');
90+
});
91+
92+
itRenders('class child with wrong context', async render => {
93+
class ClassChildWithWrongContext extends React.Component {
94+
static contextType = Context;
95+
render() {
96+
// this should render blank; context.foo isn't passed to this component.
97+
return <div id="classWrongChild">{this.context.foo}</div>;
98+
}
99+
}
100+
101+
const e = await render(
102+
<PurpleContext>
103+
<ClassChildWithWrongContext />
104+
</PurpleContext>,
105+
);
106+
expect(e.textContent).toBe('');
107+
});
108+
109+
itRenders('with context passed through to a grandchild', async render => {
110+
class Grandchild extends React.Component {
111+
static contextType = Context;
112+
render() {
113+
return <div>{this.context}</div>;
114+
}
115+
}
116+
117+
const Child = props => <Grandchild />;
118+
119+
const e = await render(
120+
<PurpleContext>
121+
<Child />
122+
</PurpleContext>,
123+
);
124+
expect(e.textContent).toBe('purple');
125+
});
126+
127+
itRenders('a child context overriding a parent context', async render => {
128+
class Grandchild extends React.Component {
129+
static contextType = Context;
130+
render() {
131+
return <div>{this.context}</div>;
132+
}
133+
}
134+
135+
const e = await render(
136+
<PurpleContext>
137+
<RedContext>
138+
<Grandchild />
139+
</RedContext>
140+
</PurpleContext>,
141+
);
142+
expect(e.textContent).toBe('red');
143+
});
144+
145+
itRenders('multiple contexts', async render => {
146+
const Theme = React.createContext('dark');
147+
const Language = React.createContext('french');
148+
class Parent extends React.Component {
149+
render() {
150+
return (
151+
<Theme.Provider value="light">
152+
<Child />
153+
</Theme.Provider>
154+
);
155+
}
156+
}
157+
158+
function Child() {
159+
return (
160+
<Language.Provider value="english">
161+
<Grandchild />
162+
</Language.Provider>
163+
);
164+
}
165+
166+
class ThemeComponent extends React.Component {
167+
static contextType = Theme;
168+
render() {
169+
return <div id="theme">{this.context}</div>;
170+
}
171+
}
172+
173+
class LanguageComponent extends React.Component {
174+
static contextType = Language;
175+
render() {
176+
return <div id="language">{this.context}</div>;
177+
}
178+
}
179+
180+
const Grandchild = props => {
181+
return (
182+
<div>
183+
<ThemeComponent />
184+
<LanguageComponent />
185+
</div>
186+
);
187+
};
188+
189+
const e = await render(<Parent />);
190+
expect(e.querySelector('#theme').textContent).toBe('light');
191+
expect(e.querySelector('#language').textContent).toBe('english');
192+
});
193+
194+
itRenders('nested context unwinding', async render => {
195+
const Theme = React.createContext('dark');
196+
const Language = React.createContext('french');
197+
198+
class ThemeConsumer extends React.Component {
199+
static contextType = Theme;
200+
render() {
201+
return this.props.children(this.context);
202+
}
203+
}
204+
205+
class LanguageConsumer extends React.Component {
206+
static contextType = Language;
207+
render() {
208+
return this.props.children(this.context);
209+
}
210+
}
211+
212+
const App = () => (
213+
<div>
214+
<Theme.Provider value="light">
215+
<Language.Provider value="english">
216+
<Theme.Provider value="dark">
217+
<ThemeConsumer>
218+
{theme => <div id="theme1">{theme}</div>}
219+
</ThemeConsumer>
220+
</Theme.Provider>
221+
<ThemeConsumer>
222+
{theme => <div id="theme2">{theme}</div>}
223+
</ThemeConsumer>
224+
<Language.Provider value="sanskrit">
225+
<Theme.Provider value="blue">
226+
<Theme.Provider value="red">
227+
<LanguageConsumer>
228+
{() => (
229+
<Language.Provider value="chinese">
230+
<Language.Provider value="hungarian" />
231+
<LanguageConsumer>
232+
{language => <div id="language1">{language}</div>}
233+
</LanguageConsumer>
234+
</Language.Provider>
235+
)}
236+
</LanguageConsumer>
237+
</Theme.Provider>
238+
<LanguageConsumer>
239+
{language => (
240+
<React.Fragment>
241+
<ThemeConsumer>
242+
{theme => <div id="theme3">{theme}</div>}
243+
</ThemeConsumer>
244+
<div id="language2">{language}</div>
245+
</React.Fragment>
246+
)}
247+
</LanguageConsumer>
248+
</Theme.Provider>
249+
</Language.Provider>
250+
</Language.Provider>
251+
</Theme.Provider>
252+
<LanguageConsumer>
253+
{language => <div id="language3">{language}</div>}
254+
</LanguageConsumer>
255+
</div>
256+
);
257+
let e = await render(<App />);
258+
expect(e.querySelector('#theme1').textContent).toBe('dark');
259+
expect(e.querySelector('#theme2').textContent).toBe('light');
260+
expect(e.querySelector('#theme3').textContent).toBe('blue');
261+
expect(e.querySelector('#language1').textContent).toBe('chinese');
262+
expect(e.querySelector('#language2').textContent).toBe('sanskrit');
263+
expect(e.querySelector('#language3').textContent).toBe('french');
264+
});
265+
});
266+
});

packages/react-dom/src/server/ReactPartialRenderer.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ const didWarnAboutBadClass = {};
180180
const didWarnAboutDeprecatedWillMount = {};
181181
const didWarnAboutUndefinedDerivedState = {};
182182
const didWarnAboutUninitializedState = {};
183+
const didWarnAboutInvalidateContextType = {};
183184
const valuePropNames = ['value', 'defaultValue'];
184185
const newlineEatingTags = {
185186
listing: true,
@@ -357,13 +358,33 @@ function checkContextTypes(typeSpecs, values, location: string) {
357358
}
358359

359360
function processContext(type, context) {
360-
const maskedContext = maskContext(type, context);
361-
if (__DEV__) {
362-
if (type.contextTypes) {
363-
checkContextTypes(type.contextTypes, maskedContext, 'context');
361+
const contextType = type.contextType;
362+
if (typeof contextType === 'object' && contextType !== null) {
363+
if (__DEV__) {
364+
if (contextType.$$typeof !== REACT_CONTEXT_TYPE) {
365+
let name = getComponentName(type) || 'Component';
366+
if (!didWarnAboutInvalidateContextType[name]) {
367+
didWarnAboutInvalidateContextType[type] = true;
368+
warningWithoutStack(
369+
false,
370+
'%s defines an invalid contextType. ' +
371+
'contextType should point to the Context object returned by React.createContext(). ' +
372+
'Did you accidentally pass the Context.Provider instead?',
373+
name,
374+
);
375+
}
376+
}
364377
}
378+
return contextType._currentValue;
379+
} else {
380+
const maskedContext = maskContext(type, context);
381+
if (__DEV__) {
382+
if (type.contextTypes) {
383+
checkContextTypes(type.contextTypes, maskedContext, 'context');
384+
}
385+
}
386+
return maskedContext;
365387
}
366-
return maskedContext;
367388
}
368389

369390
const hasOwnProperty = Object.prototype.hasOwnProperty;

packages/react-reconciler/src/ReactFiberClassComponent.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import shallowEqual from 'shared/shallowEqual';
2525
import getComponentName from 'shared/getComponentName';
2626
import invariant from 'shared/invariant';
2727
import warningWithoutStack from 'shared/warningWithoutStack';
28+
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
2829

2930
import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
3031
import {StrictMode} from './ReactTypeOfMode';
32+
3133
import {
3234
enqueueUpdate,
3335
processUpdateQueue,
@@ -516,7 +518,7 @@ function constructClassInstance(
516518
if (typeof contextType === 'object' && contextType !== null) {
517519
if (__DEV__) {
518520
if (
519-
contextType.Consumer === undefined &&
521+
contextType.$$typeof !== REACT_CONTEXT_TYPE &&
520522
!didWarnAboutInvalidateContextType.has(ctor)
521523
) {
522524
didWarnAboutInvalidateContextType.add(ctor);

0 commit comments

Comments
 (0)