Skip to content

Commit 14fd963

Browse files
authored
Switch <Context> to mean <Context.Provider> (#28226)
Previously, `<Context>` was equivalent to `<Context.Consumer>`. However, since the introduction of Hooks, the `<Context.Consumer>` API is rarely used. The goal here is to make the common case cleaner: ```js const ThemeContext = createContext('light') function App() { return ( <ThemeContext value="dark"> ... </ThemeContext> ) } function Button() { const theme = use(ThemeContext) // ... } ``` This is technically a breaking change, but we've been warning about rendering `<Context>` directly for several years by now, so it's unlikely much code in the wild depends on the old behavior. [Proof that it warns today (check console).](https://codesandbox.io/p/sandbox/peaceful-nobel-pdxtfl) --- **The relevant commit is 5696782.** It switches `createContext` implementation so that `Context.Provider === Context`. The main assumption that changed is that a Provider's fiber type is now the context itself (rather than an intermediate object). Whereas a Consumer's fiber type is now always an intermediate object (rather than it being sometimes the context itself and sometimes an intermediate object). My methodology was to start with the relevant symbols, work tags, and types, and work my way backwards to all usages. This might break tooling that depends on inspecting React's internal fields. I've added DevTools support in the second commit. This didn't need explicit versioning—the structure tells us enough.
1 parent 32df74d commit 14fd963

33 files changed

+400
-473
lines changed

packages/react-client/src/ReactFlightReplyClient.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import type {
1414
RejectedThenable,
1515
ReactCustomFormAction,
1616
} from 'shared/ReactTypes';
17+
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
1718

1819
import {
1920
REACT_ELEMENT_TYPE,
2021
REACT_LAZY_TYPE,
22+
REACT_CONTEXT_TYPE,
2123
REACT_PROVIDER_TYPE,
2224
getIteratorFn,
2325
} from 'shared/ReactSymbols';
@@ -302,7 +304,10 @@ export function processReply(
302304
'React Lazy cannot be passed to Server Functions from the Client.%s',
303305
describeObjectForErrorMessage(parent, key),
304306
);
305-
} else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
307+
} else if (
308+
(value: any).$$typeof ===
309+
(enableRenderableContext ? REACT_CONTEXT_TYPE : REACT_PROVIDER_TYPE)
310+
) {
306311
console.error(
307312
'React Context Providers cannot be passed to Server Functions from the Client.%s',
308313
describeObjectForErrorMessage(parent, key),

packages/react-debug-tools/src/ReactDebugHooks.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import type {
1111
Awaited,
1212
ReactContext,
13-
ReactProviderType,
1413
StartTransitionOptions,
1514
Usable,
1615
Thenable,
@@ -931,8 +930,11 @@ function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
931930
let current: null | Fiber = fiber;
932931
while (current) {
933932
if (current.tag === ContextProvider) {
934-
const providerType: ReactProviderType<any> = current.type;
935-
const context: ReactContext<any> = providerType._context;
933+
let context: ReactContext<any> = current.type;
934+
if ((context: any)._context !== undefined) {
935+
// Support inspection of pre-19+ providers.
936+
context = (context: any)._context;
937+
}
936938
if (!contextMap.has(context)) {
937939
// Store the current value that we're going to restore later.
938940
contextMap.set(context, context._currentValue);

packages/react-devtools-shared/src/backend/ReactSymbols.js

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const PROFILER_SYMBOL_STRING = 'Symbol(react.profiler)';
5151
export const PROVIDER_NUMBER = 0xeacd;
5252
export const PROVIDER_SYMBOL_STRING = 'Symbol(react.provider)';
5353

54+
export const CONSUMER_SYMBOL_STRING = 'Symbol(react.consumer)';
55+
5456
export const SCOPE_NUMBER = 0xead7;
5557
export const SCOPE_SYMBOL_STRING = 'Symbol(react.scope)';
5658

packages/react-devtools-shared/src/backend/renderer.js

+51-2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
PROVIDER_SYMBOL_STRING,
8080
CONTEXT_NUMBER,
8181
CONTEXT_SYMBOL_STRING,
82+
CONSUMER_SYMBOL_STRING,
8283
STRICT_MODE_NUMBER,
8384
STRICT_MODE_SYMBOL_STRING,
8485
PROFILER_NUMBER,
@@ -525,6 +526,15 @@ export function getInternalReactConstants(version: string): {
525526
case CONTEXT_NUMBER:
526527
case CONTEXT_SYMBOL_STRING:
527528
case SERVER_CONTEXT_SYMBOL_STRING:
529+
if (
530+
fiber.type._context === undefined &&
531+
fiber.type.Provider === fiber.type
532+
) {
533+
// In 19+, Context.Provider === Context, so this is a provider.
534+
resolvedContext = fiber.type;
535+
return `${resolvedContext.displayName || 'Context'}.Provider`;
536+
}
537+
528538
// 16.3-16.5 read from "type" because the Consumer is the actual context object.
529539
// 16.6+ should read from "type._context" because Consumer can be different (in DEV).
530540
// NOTE Keep in sync with inspectElementRaw()
@@ -533,6 +543,10 @@ export function getInternalReactConstants(version: string): {
533543
// NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer'
534544
// If you change the name, figure out a more resilient way to detect it.
535545
return `${resolvedContext.displayName || 'Context'}.Consumer`;
546+
case CONSUMER_SYMBOL_STRING:
547+
// 19+
548+
resolvedContext = fiber.type._context;
549+
return `${resolvedContext.displayName || 'Context'}.Consumer`;
536550
case STRICT_MODE_NUMBER:
537551
case STRICT_MODE_SYMBOL_STRING:
538552
return null;
@@ -3178,8 +3192,14 @@ export function attach(
31783192
}
31793193
}
31803194
} else if (
3181-
typeSymbol === CONTEXT_NUMBER ||
3182-
typeSymbol === CONTEXT_SYMBOL_STRING
3195+
// Detect pre-19 Context Consumers
3196+
(typeSymbol === CONTEXT_NUMBER || typeSymbol === CONTEXT_SYMBOL_STRING) &&
3197+
!(
3198+
// In 19+, CONTEXT_SYMBOL_STRING means a Provider instead.
3199+
// It will be handled in a different branch below.
3200+
// Eventually, this entire branch can be removed.
3201+
(type._context === undefined && type.Provider === type)
3202+
)
31833203
) {
31843204
// 16.3-16.5 read from "type" because the Consumer is the actual context object.
31853205
// 16.6+ should read from "type._context" because Consumer can be different (in DEV).
@@ -3209,6 +3229,35 @@ export function attach(
32093229
}
32103230
}
32113231

3232+
current = current.return;
3233+
}
3234+
} else if (
3235+
// Detect 19+ Context Consumers
3236+
typeSymbol === CONSUMER_SYMBOL_STRING
3237+
) {
3238+
// This branch is 19+ only, where Context.Provider === Context.
3239+
// NOTE Keep in sync with getDisplayNameForFiber()
3240+
const consumerResolvedContext = type._context;
3241+
3242+
// Global context value.
3243+
context = consumerResolvedContext._currentValue || null;
3244+
3245+
// Look for overridden value.
3246+
let current = ((fiber: any): Fiber).return;
3247+
while (current !== null) {
3248+
const currentType = current.type;
3249+
const currentTypeSymbol = getTypeSymbol(currentType);
3250+
if (
3251+
// In 19+, these are Context Providers
3252+
currentTypeSymbol === CONTEXT_SYMBOL_STRING
3253+
) {
3254+
const providerResolvedContext = currentType;
3255+
if (providerResolvedContext === consumerResolvedContext) {
3256+
context = current.memoizedProps.value;
3257+
break;
3258+
}
3259+
}
3260+
32123261
current = current.return;
32133262
}
32143263
}

packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js

+26-107
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ function initModules() {
3131
};
3232
}
3333

34-
const {resetModules, itRenders, clientRenderOnBadMarkup} =
35-
ReactDOMServerIntegrationUtils(initModules);
34+
const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
3635

3736
describe('ReactDOMServerIntegration', () => {
3837
beforeEach(() => {
@@ -296,115 +295,35 @@ describe('ReactDOMServerIntegration', () => {
296295
expect(e.querySelector('#language3').textContent).toBe('french');
297296
});
298297

299-
itRenders(
300-
'should warn with an error message when using Context as consumer in DEV',
301-
async render => {
302-
const Theme = React.createContext('dark');
303-
const Language = React.createContext('french');
298+
itRenders('should treat Context as Context.Provider', async render => {
299+
// The `itRenders` helpers don't work with the gate pragma, so we have to do
300+
// this instead.
301+
if (gate(flags => !flags.enableRenderableContext)) {
302+
return;
303+
}
304304

305-
const App = () => (
306-
<div>
307-
<Theme.Provider value="light">
308-
<Language.Provider value="english">
309-
<Theme.Provider value="dark">
310-
<Theme>{theme => <div id="theme1">{theme}</div>}</Theme>
311-
</Theme.Provider>
312-
</Language.Provider>
313-
</Theme.Provider>
314-
</div>
315-
);
316-
// We expect 1 error.
317-
await render(<App />, 1);
318-
},
319-
);
320-
321-
// False positive regression test.
322-
itRenders(
323-
'should not warn when using Consumer from React < 16.6 with newer renderer',
324-
async render => {
325-
const Theme = React.createContext('dark');
326-
const Language = React.createContext('french');
327-
// React 16.5 and earlier didn't have a separate object.
328-
Theme.Consumer = Theme;
329-
330-
const App = () => (
331-
<div>
332-
<Theme.Provider value="light">
333-
<Language.Provider value="english">
334-
<Theme.Provider value="dark">
335-
<Theme>{theme => <div id="theme1">{theme}</div>}</Theme>
336-
</Theme.Provider>
337-
</Language.Provider>
338-
</Theme.Provider>
339-
</div>
340-
);
341-
// We expect 0 errors.
342-
await render(<App />, 0);
343-
},
344-
);
345-
346-
itRenders(
347-
'should warn with an error message when using nested context consumers in DEV',
348-
async render => {
349-
const App = () => {
350-
const Theme = React.createContext('dark');
351-
const Language = React.createContext('french');
305+
const Theme = React.createContext('dark');
306+
const Language = React.createContext('french');
352307

353-
return (
354-
<div>
355-
<Theme.Provider value="light">
356-
<Language.Provider value="english">
357-
<Theme.Provider value="dark">
358-
<Theme.Consumer.Consumer>
359-
{theme => <div id="theme1">{theme}</div>}
360-
</Theme.Consumer.Consumer>
361-
</Theme.Provider>
362-
</Language.Provider>
363-
</Theme.Provider>
364-
</div>
365-
);
366-
};
367-
await render(
368-
<App />,
369-
render === clientRenderOnBadMarkup
370-
? // On hydration mismatch we retry and therefore log the warning again.
371-
2
372-
: 1,
373-
);
374-
},
375-
);
308+
expect(Theme.Provider).toBe(Theme);
376309

377-
itRenders(
378-
'should warn with an error message when using Context.Consumer.Provider DEV',
379-
async render => {
380-
const App = () => {
381-
const Theme = React.createContext('dark');
382-
const Language = React.createContext('french');
310+
const App = () => (
311+
<div>
312+
<Theme value="light">
313+
<Language value="english">
314+
<Theme value="dark">
315+
<Theme.Consumer>
316+
{theme => <div id="theme1">{theme}</div>}
317+
</Theme.Consumer>
318+
</Theme>
319+
</Language>
320+
</Theme>
321+
</div>
322+
);
383323

384-
return (
385-
<div>
386-
<Theme.Provider value="light">
387-
<Language.Provider value="english">
388-
<Theme.Consumer.Provider value="dark">
389-
<Theme.Consumer>
390-
{theme => <div id="theme1">{theme}</div>}
391-
</Theme.Consumer>
392-
</Theme.Consumer.Provider>
393-
</Language.Provider>
394-
</Theme.Provider>
395-
</div>
396-
);
397-
};
398-
399-
await render(
400-
<App />,
401-
render === clientRenderOnBadMarkup
402-
? // On hydration mismatch we retry and therefore log the warning again.
403-
2
404-
: 1,
405-
);
406-
},
407-
);
324+
const e = await render(<App />, 0);
325+
expect(e.textContent).toBe('dark');
326+
});
408327

409328
it('does not pollute parallel node streams', () => {
410329
const LoggedInUser = React.createContext();

packages/react-dom/src/__tests__/ReactServerRendering-test.js

+9-15
Original file line numberDiff line numberDiff line change
@@ -1000,22 +1000,15 @@ describe('ReactDOMServer', () => {
10001000
]);
10011001
});
10021002

1003+
// @gate enableRenderableContext || !__DEV__
10031004
it('should warn if an invalid contextType is defined', () => {
10041005
const Context = React.createContext();
1005-
10061006
class ComponentA extends React.Component {
1007-
// It should warn for both Context.Consumer and Context.Provider
10081007
static contextType = Context.Consumer;
10091008
render() {
10101009
return <div />;
10111010
}
10121011
}
1013-
class ComponentB extends React.Component {
1014-
static contextType = Context.Provider;
1015-
render() {
1016-
return <div />;
1017-
}
1018-
}
10191012

10201013
expect(() => {
10211014
ReactDOMServer.renderToString(<ComponentA />);
@@ -1028,13 +1021,14 @@ describe('ReactDOMServer', () => {
10281021
// Warnings should be deduped by component type
10291022
ReactDOMServer.renderToString(<ComponentA />);
10301023

1031-
expect(() => {
1032-
ReactDOMServer.renderToString(<ComponentB />);
1033-
}).toErrorDev(
1034-
'Warning: ComponentB defines an invalid contextType. ' +
1035-
'contextType should point to the Context object returned by React.createContext(). ' +
1036-
'Did you accidentally pass the Context.Provider instead?',
1037-
);
1024+
class ComponentB extends React.Component {
1025+
static contextType = Context.Provider;
1026+
render() {
1027+
return <div />;
1028+
}
1029+
}
1030+
// Does not warn because Context === Context.Provider.
1031+
ReactDOMServer.renderToString(<ComponentB />);
10381032
});
10391033

10401034
it('should not warn when class contextType is null', () => {

0 commit comments

Comments
 (0)