From d5f1b067c8bbb826b823d0354a28ba31078b70c0 Mon Sep 17 00:00:00 2001 From: salazarm Date: Tue, 8 Mar 2022 07:55:32 -0500 Subject: [PATCH] [ServerContext] Flight support for ServerContext (#23244) * Flight side of server context * 1 more test * rm unused function * flow+prettier * flow again =) * duplicate ReactServerContext across packages * store default value when lazily initializing server context * . * better comment * derp... missing import * rm optional chaining * missed feature flag * React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ?? * add warning if non ServerContext passed into useServerContext * pass context in as array of arrays * make importServerContext nott pollute the global context state * merge main * remove useServerContext * dont rely on object getters in ReactServerContext and disallow JSX * add symbols to devtools + rename globalServerContextRegistry to just ContextRegistry * gate test case as experimental * feedback * remove unions * Lint * fix oopsies (tests/lint/mismatching arguments/signatures * lint again * replace-fork * remove extraneous change * rebase * 1 more test * rm unused function * flow+prettier * flow again =) * duplicate ReactServerContext across packages * store default value when lazily initializing server context * . * better comment * derp... missing import * rm optional chaining * missed feature flag * React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ?? * add warning if non ServerContext passed into useServerContext * pass context in as array of arrays * make importServerContext nott pollute the global context state * merge main * remove useServerContext * dont rely on object getters in ReactServerContext and disallow JSX * add symbols to devtools + rename globalServerContextRegistry to just ContextRegistry * gate test case as experimental * feedback * remove unions * Lint * fix oopsies (tests/lint/mismatching arguments/signatures * lint again * replace-fork * remove extraneous change * rebase * reinline * rebase * add back changes lost due to rebase being hard * emit chunk for provider * remove case for React provider type * update type for SomeChunk * enable flag with experimental * add missing types * fix flow type * missing type * t: any * revert extraneous type change * better type * better type * feedback * change import to type import * test? * test? * remove react-dom * remove react-native-renderer from react-server-native-relay/package.json * gate change in FiberNewContext, getComponentNameFromType, use switch statement in FlightServer * getComponentNameFromTpe: server context type gated and use displayName if available * fallthrough * lint.... * POP * lint --- .../react-client/src/ReactFlightClient.js | 18 + .../src/ReactFlightClientStream.js | 5 + .../src/__tests__/ReactFlight-test.js | 352 ++++++++++++++++++ .../src/backend/ReactSymbols.js | 5 + .../src/backend/renderer.js | 2 + .../src/__tests__/ReactDOMFizzServer-test.js | 49 +++ packages/react-is/src/ReactIs.js | 2 + .../src/ReactNoopFlightServer.js | 8 +- .../src/ReactFiberCacheComponent.new.js | 2 + .../src/ReactFiberCacheComponent.old.js | 2 + .../src/ReactFiberHooks.new.js | 3 +- .../src/ReactFiberHooks.old.js | 3 +- .../src/ReactFiberNewContext.new.js | 20 +- .../src/ReactFiberNewContext.old.js | 20 +- .../src/ReactInternalTypes.js | 2 +- .../src/ReactFlightDOMRelayProtocol.js | 1 + .../ReactFlightDOMRelayServerHostConfig.js | 8 + .../react-server-dom-webpack/package.json | 1 - .../src/ReactFlightDOMServerBrowser.js | 3 + .../src/ReactFlightDOMServerNode.js | 3 + .../react-server-native-relay/package.json | 3 +- .../src/ReactFlightNativeRelayProtocol.js | 1 + .../ReactFlightNativeRelayServerHostConfig.js | 8 + .../react-server/src/ReactFizzNewContext.js | 15 +- packages/react-server/src/ReactFlightHooks.js | 93 +++++ .../react-server/src/ReactFlightNewContext.js | 269 +++++++++++++ .../react-server/src/ReactFlightServer.js | 202 ++++++---- .../src/ReactFlightServerConfigStream.js | 9 + packages/react/index.classic.fb.js | 3 +- packages/react/index.experimental.js | 3 +- packages/react/index.js | 1 + packages/react/index.modern.fb.js | 3 +- packages/react/index.stable.js | 2 +- packages/react/src/React.js | 2 + packages/react/src/ReactContext.js | 4 + packages/react/src/ReactServerContext.js | 104 ++++++ .../react/src/ReactServerContextRegistry.js | 5 + packages/react/src/ReactSharedInternals.js | 6 + .../src/forks/ReactSharedInternals.umd.js | 6 + packages/shared/ReactFeatureFlags.js | 1 + packages/shared/ReactServerContextRegistry.js | 24 ++ packages/shared/ReactSymbols.js | 4 + packages/shared/ReactTypes.js | 14 + .../forks/ReactFeatureFlags.native-fb.js | 2 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 2 +- .../shared/forks/ReactFeatureFlags.www.js | 1 + packages/shared/getComponentNameFromType.js | 13 +- scripts/error-codes/codes.json | 18 +- 53 files changed, 1242 insertions(+), 90 deletions(-) create mode 100644 packages/react-server/src/ReactFlightHooks.js create mode 100644 packages/react-server/src/ReactFlightNewContext.js create mode 100644 packages/react/src/ReactServerContext.js create mode 100644 packages/react/src/ReactServerContextRegistry.js create mode 100644 packages/shared/ReactServerContextRegistry.js diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 9d5ac3680a8a..dabb215c708f 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -26,6 +26,8 @@ import { import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; +import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; + export type JSONValue = | number | null @@ -327,6 +329,7 @@ export function parseModelTuple( value: {+[key: string]: JSONValue} | $ReadOnlyArray, ): any { const tuple: [mixed, mixed, mixed, mixed] = (value: any); + if (tuple[0] === REACT_ELEMENT_TYPE) { // TODO: Consider having React just directly accept these arrays as elements. // Or even change the ReactElement type to be an array. @@ -358,6 +361,21 @@ export function resolveModel( } } +export function resolveProvider( + response: Response, + id: number, + contextName: string, +): void { + const chunks = response._chunks; + chunks.set( + id, + createInitializedChunk( + response, + getOrCreateServerContext(contextName).Provider, + ), + ); +} + export function resolveModule( response: Response, id: number, diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js index 9f07d8cc999a..8af1734de6b7 100644 --- a/packages/react-client/src/ReactFlightClientStream.js +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -12,6 +12,7 @@ import type {Response} from './ReactFlightClientHostConfigStream'; import { resolveModule, resolveModel, + resolveProvider, resolveSymbol, resolveError, createResponse as createResponseBase, @@ -49,6 +50,10 @@ function processFullRow(response: Response, row: string): void { resolveModule(response, id, text); return; } + case 'P': { + resolveProvider(response, id, text); + return; + } case 'S': { resolveSymbol(response, id, JSON.parse(text)); return; diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 8d0fbe1609d9..9197a4c3bfe5 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -17,6 +17,8 @@ let ReactNoopFlightServer; let ReactNoopFlightClient; let ErrorBoundary; let NoErrorExpected; +let Scheduler; +let ContextRegistry; describe('ReactFlight', () => { beforeEach(() => { @@ -27,6 +29,10 @@ describe('ReactFlight', () => { ReactNoopFlightServer = require('react-noop-renderer/flight-server'); ReactNoopFlightClient = require('react-noop-renderer/flight-client'); act = require('jest-react').act; + Scheduler = require('scheduler'); + const ReactSharedInternals = + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + ContextRegistry = ReactSharedInternals.ContextRegistry; ErrorBoundary = class extends React.Component { state = {hasError: false, error: null}; @@ -302,4 +308,350 @@ describe('ReactFlight', () => { {withoutStack: true}, ); }); + + describe('ServerContext', () => { + // @gate enableServerContext + it('supports basic createServerContext usage', () => { + const ServerContext = React.createServerContext( + 'ServerContext', + 'hello from server', + ); + function Foo() { + const context = React.useContext(ServerContext); + return
{context}
; + } + + const transport = ReactNoopFlightServer.render(); + act(() => { + ServerContext._currentRenderer = null; + ServerContext._currentRenderer2 = null; + ReactNoop.render(ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput(
hello from server
); + }); + + // @gate enableServerContext + it('propagates ServerContext providers in flight', () => { + const ServerContext = React.createServerContext( + 'ServerContext', + 'default', + ); + + function Foo() { + return ( +
+ + + +
+ ); + } + function Bar() { + const context = React.useContext(ServerContext); + return context; + } + + const transport = ReactNoopFlightServer.render(); + act(() => { + ServerContext._currentRenderer = null; + ServerContext._currentRenderer2 = null; + ReactNoop.render(ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput(
hi this is server
); + }); + + // @gate enableServerContext + it('errors if you try passing JSX through ServerContext value', () => { + const ServerContext = React.createServerContext('ServerContext', { + foo: { + bar: hi this is default, + }, + }); + + function Foo() { + return ( +
+ hi this is server, + }, + }}> + + +
+ ); + } + function Bar() { + const context = React.useContext(ServerContext); + return context.foo.bar; + } + + expect(() => { + ReactNoopFlightServer.render(); + }).toErrorDev('React elements are not allowed in ServerContext', { + withoutStack: true, + }); + }); + + // @gate enableServerContext + it('propagates ServerContext and cleansup providers in flight', () => { + const ServerContext = React.createServerContext( + 'ServerContext', + 'default', + ); + + function Foo() { + return ( + <> + + + + + + + + + + + + + + + ); + } + function Bar() { + const context = React.useContext(ServerContext); + return {context}; + } + + const transport = ReactNoopFlightServer.render(); + act(() => { + ServerContext._currentRenderer = null; + ServerContext._currentRenderer2 = null; + ReactNoop.render(ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> + hi this is server + hi this is server2 + hi this is server outer + hi this is server outer2 + default + , + ); + }); + + // @gate enableServerContext + it('propagates ServerContext providers in flight after suspending', async () => { + const ServerContext = React.createServerContext( + 'ServerContext', + 'default', + ); + + function Foo() { + return ( +
+ + + + + +
+ ); + } + + let resolve; + const promise = new Promise(res => { + resolve = () => { + promise.unsuspend = true; + res(); + }; + }); + + function Bar() { + if (!promise.unsuspend) { + Scheduler.unstable_yieldValue('suspended'); + throw promise; + } + Scheduler.unstable_yieldValue('rendered'); + const context = React.useContext(ServerContext); + return context; + } + + const transport = ReactNoopFlightServer.render(); + + expect(Scheduler).toHaveYielded(['suspended']); + + await act(async () => { + resolve(); + await promise; + jest.runAllImmediates(); + }); + + expect(Scheduler).toHaveYielded(['rendered']); + + act(() => { + ServerContext._currentRenderer = null; + ServerContext._currentRenderer2 = null; + ReactNoop.render(ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput(
hi this is server
); + }); + + // @gate enableServerContext + it('serializes ServerContext to client', async () => { + const ServerContext = React.createServerContext( + 'ServerContext', + 'default', + ); + + function ClientBar() { + Scheduler.unstable_yieldValue('ClientBar'); + const context = React.useContext(ServerContext); + return {context}; + } + + const Bar = moduleReference(ClientBar); + + function Foo() { + return ( + + + + ); + } + + const model = { + foo: , + }; + + const transport = ReactNoopFlightServer.render(model); + + expect(Scheduler).toHaveYielded([]); + + act(() => { + ServerContext._currentRenderer = null; + ServerContext._currentRenderer2 = null; + const flightModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(flightModel.foo); + }); + + expect(Scheduler).toHaveYielded(['ClientBar']); + expect(ReactNoop).toMatchRenderedOutput(hi this is server); + + expect(() => { + React.createServerContext('ServerContext', 'default'); + }).toThrow('ServerContext: ServerContext already defined'); + }); + + // @gate enableServerContext + it('takes ServerContext from client for refetching usecases', async () => { + const ServerContext = React.createServerContext( + 'ServerContext', + 'default', + ); + function Bar() { + return {React.useContext(ServerContext)}; + } + const transport = ReactNoopFlightServer.render(, {}, [ + ['ServerContext', 'Override'], + ]); + + act(() => { + const flightModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(flightModel); + }); + + expect(ReactNoop).toMatchRenderedOutput(Override); + }); + + // @gate enableServerContext + it('sets default initial value when defined lazily on server or client', async () => { + let ServerContext; + function inlineLazyServerContextInitialization() { + if (!ServerContext) { + ServerContext = React.createServerContext('ServerContext', 'default'); + } + return ServerContext; + } + + let ClientContext; + function inlineContextInitialization() { + if (!ClientContext) { + ClientContext = React.createServerContext('ServerContext', 'default'); + } + return ClientContext; + } + + function ClientBaz() { + const context = inlineContextInitialization(); + const value = React.useContext(context); + return
{value}
; + } + + const Baz = moduleReference(ClientBaz); + + function Bar() { + return ( +
+
+ {React.useContext(inlineLazyServerContextInitialization())} +
+ +
+ ); + } + + function ServerApp() { + const Context = inlineLazyServerContextInitialization(); + return ( + <> + + + + + + ); + } + + function ClientApp({serverModel}) { + return ( + <> + {serverModel} + + + ); + } + + const transport = ReactNoopFlightServer.render(); + + expect(ClientContext).toBe(undefined); + act(() => { + delete ContextRegistry.ServerContext; + ServerContext._currentRenderer = null; + ServerContext._currentRenderer2 = null; + const serverModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> +
+
test
+
test
+
+
+
default
+
default
+
+
default
+ , + ); + }); + }); }); diff --git a/packages/react-devtools-shared/src/backend/ReactSymbols.js b/packages/react-devtools-shared/src/backend/ReactSymbols.js index ebc6920be8d2..775ad34e7720 100644 --- a/packages/react-devtools-shared/src/backend/ReactSymbols.js +++ b/packages/react-devtools-shared/src/backend/ReactSymbols.js @@ -19,6 +19,8 @@ export const CONCURRENT_MODE_SYMBOL_STRING = 'Symbol(react.concurrent_mode)'; export const CONTEXT_NUMBER = 0xeace; export const CONTEXT_SYMBOL_STRING = 'Symbol(react.context)'; +export const SERVER_CONTEXT_SYMBOL_STRING = 'Symbol(react.server_context)'; + export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = 'Symbol(react.async_mode)'; export const ELEMENT_NUMBER = 0xeac7; @@ -60,3 +62,6 @@ export const SUSPENSE_SYMBOL_STRING = 'Symbol(react.suspense)'; export const SUSPENSE_LIST_NUMBER = 0xead8; export const SUSPENSE_LIST_SYMBOL_STRING = 'Symbol(react.suspense_list)'; + +export const SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED_SYMBOL_STRING = + 'Symbol(react.server_context.defaultValue)'; diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 88c1d5360837..665b85d183a3 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -85,6 +85,7 @@ import { FORWARD_REF_SYMBOL_STRING, MEMO_NUMBER, MEMO_SYMBOL_STRING, + SERVER_CONTEXT_SYMBOL_STRING, } from './ReactSymbols'; import {format} from './utils'; import { @@ -511,6 +512,7 @@ export function getInternalReactConstants( return `${resolvedContext.displayName || 'Context'}.Provider`; case CONTEXT_NUMBER: case CONTEXT_SYMBOL_STRING: + case SERVER_CONTEXT_SYMBOL_STRING: // 16.3-16.5 read from "type" because the Consumer is the actual context object. // 16.6+ should read from "type._context" because Consumer can be different (in DEV). // NOTE Keep in sync with inspectElementRaw() diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 0aacf5a75524..06e83ba128be 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -2455,4 +2455,53 @@ describe('ReactDOMFizzServer', () => { 'Suspense boundary. Switched to client rendering.', ]); }); + + // @gate enableServerContext && experimental + it('supports ServerContext', async () => { + let ServerContext; + function inlineLazyServerContextInitialization() { + if (!ServerContext) { + ServerContext = React.createServerContext('ServerContext', 'default'); + } + return ServerContext; + } + + function Foo() { + inlineLazyServerContextInitialization(); + return ( + <> + + + + + + + + + + + + + + + ); + } + function Bar() { + const context = React.useContext(inlineLazyServerContextInitialization()); + return {context}; + } + + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + + expect(getVisibleChildren(container)).toEqual([ + hi this is server, + hi this is server2, + hi this is server outer, + hi this is server outer2, + default, + ]); + }); }); diff --git a/packages/react-is/src/ReactIs.js b/packages/react-is/src/ReactIs.js index dd81ec036158..8c5d26a08c2a 100644 --- a/packages/react-is/src/ReactIs.js +++ b/packages/react-is/src/ReactIs.js @@ -11,6 +11,7 @@ import { REACT_CONTEXT_TYPE, + REACT_SERVER_CONTEXT_TYPE, REACT_ELEMENT_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, @@ -43,6 +44,7 @@ export function typeOf(object: any) { const $$typeofType = type && type.$$typeof; switch ($$typeofType) { + case REACT_SERVER_CONTEXT_TYPE: case REACT_CONTEXT_TYPE: case REACT_FORWARD_REF_TYPE: case REACT_LAZY_TYPE: diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index ca6fedf2ec63..1c607befe74b 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -15,6 +15,7 @@ */ import type {ReactModel} from 'react-server/src/ReactFlightServer'; +import type {ServerContextJSONValue} from 'shared/ReactTypes'; import {saveModule} from 'react-noop-renderer/flight-modules'; @@ -62,13 +63,18 @@ type Options = { onError?: (error: mixed) => void, }; -function render(model: ReactModel, options?: Options): Destination { +function render( + model: ReactModel, + options?: Options, + context?: Array<[string, ServerContextJSONValue]>, +): Destination { const destination: Destination = []; const bundlerConfig = undefined; const request = ReactNoopFlightServer.createRequest( model, bundlerConfig, options ? options.onError : undefined, + context, ); ReactNoopFlightServer.startWork(request); ReactNoopFlightServer.startFlowing(request, destination); diff --git a/packages/react-reconciler/src/ReactFiberCacheComponent.new.js b/packages/react-reconciler/src/ReactFiberCacheComponent.new.js index 14ec05bd5b46..67588c3219a9 100644 --- a/packages/react-reconciler/src/ReactFiberCacheComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberCacheComponent.new.js @@ -48,6 +48,8 @@ export const CacheContext: ReactContext = enableCache _currentValue: (null: any), _currentValue2: (null: any), _threadCount: 0, + _defaultValue: (null: any), + _globalName: (null: any), } : (null: any); diff --git a/packages/react-reconciler/src/ReactFiberCacheComponent.old.js b/packages/react-reconciler/src/ReactFiberCacheComponent.old.js index a34de4142e4c..e530619d4166 100644 --- a/packages/react-reconciler/src/ReactFiberCacheComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberCacheComponent.old.js @@ -48,6 +48,8 @@ export const CacheContext: ReactContext = enableCache _currentValue: (null: any), _currentValue2: (null: any), _threadCount: 0, + _defaultValue: (null: any), + _globalName: (null: any), } : (null: any); diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 011c64f59d2c..d1c50b42d8cb 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -2451,7 +2451,6 @@ if (enableCache) { (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh; } - const HooksDispatcherOnUpdate: Dispatcher = { readContext, @@ -3264,7 +3263,7 @@ if (__DEV__) { (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; - updateHookTypesDev(); + mountHookTypesDev(); return mountRefresh(); }; } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 48f8f14bf2e2..137ee33cba51 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -2451,7 +2451,6 @@ if (enableCache) { (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType; (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh; } - const HooksDispatcherOnUpdate: Dispatcher = { readContext, @@ -3264,7 +3263,7 @@ if (__DEV__) { (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).getCacheForType = getCacheForType; (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useCacheRefresh = function useCacheRefresh() { currentHookNameInDev = 'useCacheRefresh'; - updateHookTypesDev(); + mountHookTypesDev(); return mountRefresh(); }; } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.new.js b/packages/react-reconciler/src/ReactFiberNewContext.new.js index 8ff30c810f03..8f269a405021 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.new.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.new.js @@ -44,7 +44,9 @@ import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new'; import { enableSuspenseServerRenderer, enableLazyContextPropagation, + enableServerContext, } from 'shared/ReactFeatureFlags'; +import {REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED} from 'shared/ReactSymbols'; const valueCursor: StackCursor = createCursor(null); @@ -132,9 +134,23 @@ export function popProvider( const currentValue = valueCursor.current; pop(valueCursor, providerFiber); if (isPrimaryRenderer) { - context._currentValue = currentValue; + if ( + enableServerContext && + currentValue === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED + ) { + context._currentValue = context._defaultValue; + } else { + context._currentValue = currentValue; + } } else { - context._currentValue2 = currentValue; + if ( + enableServerContext && + currentValue === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED + ) { + context._currentValue2 = context._defaultValue; + } else { + context._currentValue2 = currentValue; + } } } diff --git a/packages/react-reconciler/src/ReactFiberNewContext.old.js b/packages/react-reconciler/src/ReactFiberNewContext.old.js index 93fe3bc8395c..a48c84204382 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.old.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.old.js @@ -44,7 +44,9 @@ import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.old'; import { enableSuspenseServerRenderer, enableLazyContextPropagation, + enableServerContext, } from 'shared/ReactFeatureFlags'; +import {REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED} from 'shared/ReactSymbols'; const valueCursor: StackCursor = createCursor(null); @@ -132,9 +134,23 @@ export function popProvider( const currentValue = valueCursor.current; pop(valueCursor, providerFiber); if (isPrimaryRenderer) { - context._currentValue = currentValue; + if ( + enableServerContext && + currentValue === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED + ) { + context._currentValue = context._defaultValue; + } else { + context._currentValue = currentValue; + } } else { - context._currentValue2 = currentValue; + if ( + enableServerContext && + currentValue === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED + ) { + context._currentValue2 = context._defaultValue; + } else { + context._currentValue2 = currentValue; + } } } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 162ba457d549..dd2e09c03b21 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -16,6 +16,7 @@ import type { MutableSourceVersion, MutableSource, StartTransitionOptions, + Wakeable, } from 'shared/ReactTypes'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import type {WorkTag} from './ReactWorkTags'; @@ -24,7 +25,6 @@ import type {Flags} from './ReactFiberFlags'; import type {Lane, Lanes, LaneMap} from './ReactFiberLane.old'; import type {RootTag} from './ReactRootTags'; import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig'; -import type {Wakeable} from 'shared/ReactTypes'; import type {Cache} from './ReactFiberCacheComponent.old'; import type {Transitions} from './ReactFiberTracingMarkerComponent.new'; diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js index 94f73b0d7a4a..60d19ecef3f7 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js @@ -20,6 +20,7 @@ export type JSONValue = export type RowEncoding = | ['J', number, JSONValue] | ['M', number, ModuleMetaData] + | ['P', number, string] | ['S', number, string] | [ 'E', diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js index 971eca0908cb..a29a6449d8b4 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js @@ -125,6 +125,14 @@ export function processModuleChunk( return ['M', id, moduleMetaData]; } +export function processProviderChunk( + request: Request, + id: number, + contextName: string, +): Chunk { + return ['P', id, contextName]; +} + export function processSymbolChunk( request: Request, id: number, diff --git a/packages/react-server-dom-webpack/package.json b/packages/react-server-dom-webpack/package.json index ababa89250e3..0a8c3389de71 100644 --- a/packages/react-server-dom-webpack/package.json +++ b/packages/react-server-dom-webpack/package.json @@ -48,7 +48,6 @@ }, "peerDependencies": { "react": "^17.0.0", - "react-dom": "^17.0.0", "webpack": "^5.59.0" }, "dependencies": { diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js index 35518c9d33bf..aeee2d24806d 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js @@ -8,6 +8,7 @@ */ import type {ReactModel} from 'react-server/src/ReactFlightServer'; +import type {ServerContextJSONValue} from 'shared/ReactTypes'; import type {BundlerConfig} from './ReactFlightServerWebpackBundlerConfig'; import { @@ -24,11 +25,13 @@ function renderToReadableStream( model: ReactModel, webpackMap: BundlerConfig, options?: Options, + context?: Array<[string, ServerContextJSONValue]>, ): ReadableStream { const request = createRequest( model, webpackMap, options ? options.onError : undefined, + context, ); const stream = new ReadableStream({ type: 'bytes', diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js index 5f992d4b03e9..c088725f2590 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js @@ -10,6 +10,7 @@ import type {ReactModel} from 'react-server/src/ReactFlightServer'; import type {BundlerConfig} from './ReactFlightServerWebpackBundlerConfig'; import type {Writable} from 'stream'; +import type {ServerContextJSONValue} from 'shared/ReactTypes'; import { createRequest, @@ -33,11 +34,13 @@ function renderToPipeableStream( model: ReactModel, webpackMap: BundlerConfig, options?: Options, + context?: Array<[string, ServerContextJSONValue]>, ): Controls { const request = createRequest( model, webpackMap, options ? options.onError : undefined, + context, ); let hasStartedFlowing = false; startWork(request); diff --git a/packages/react-server-native-relay/package.json b/packages/react-server-native-relay/package.json index 241913f91e6a..e97b2f9b2be7 100644 --- a/packages/react-server-native-relay/package.json +++ b/packages/react-server-native-relay/package.json @@ -11,7 +11,6 @@ "scheduler": "^0.11.0" }, "peerDependencies": { - "react": "^17.0.0", - "react-native-renderer": "^17.0.0" + "react": "^17.0.0" } } diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js index 75f6db8039ab..1c32ac0dd4d4 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js @@ -20,6 +20,7 @@ export type JSONValue = export type RowEncoding = | ['J', number, JSONValue] | ['M', number, ModuleMetaData] + | ['P', number, string] | ['S', number, string] | [ 'E', diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js index 0387d94ecad2..b07c084eaa69 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js @@ -122,6 +122,14 @@ export function processModuleChunk( return ['M', id, moduleMetaData]; } +export function processProviderChunk( + request: Request, + id: number, + contextName: string, +): Chunk { + return ['P', id, contextName]; +} + export function processSymbolChunk( request: Request, id: number, diff --git a/packages/react-server/src/ReactFizzNewContext.js b/packages/react-server/src/ReactFizzNewContext.js index 0eaa07f839a1..b4386399c334 100644 --- a/packages/react-server/src/ReactFizzNewContext.js +++ b/packages/react-server/src/ReactFizzNewContext.js @@ -9,6 +9,7 @@ import type {ReactContext} from 'shared/ReactTypes'; +import {REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED} from 'shared/ReactSymbols'; import {isPrimaryRenderer} from './ReactServerFormatConfig'; let rendererSigil; @@ -244,7 +245,12 @@ export function popProvider(context: ReactContext): ContextSnapshot { } } if (isPrimaryRenderer) { - prevSnapshot.context._currentValue = prevSnapshot.parentValue; + const value = prevSnapshot.parentValue; + if (value === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED) { + prevSnapshot.context._currentValue = prevSnapshot.context._defaultValue; + } else { + prevSnapshot.context._currentValue = value; + } if (__DEV__) { if ( context._currentRenderer !== undefined && @@ -259,7 +265,12 @@ export function popProvider(context: ReactContext): ContextSnapshot { context._currentRenderer = rendererSigil; } } else { - prevSnapshot.context._currentValue2 = prevSnapshot.parentValue; + const value = prevSnapshot.parentValue; + if (value === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED) { + prevSnapshot.context._currentValue2 = prevSnapshot.context._defaultValue; + } else { + prevSnapshot.context._currentValue2 = value; + } if (__DEV__) { if ( context._currentRenderer2 !== undefined && diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js new file mode 100644 index 000000000000..88a2eac86ca2 --- /dev/null +++ b/packages/react-server/src/ReactFlightHooks.js @@ -0,0 +1,93 @@ +/** + * 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. + * + * @flow + */ + +import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes'; +import type {ReactServerContext} from 'shared/ReactTypes'; +import {REACT_SERVER_CONTEXT_TYPE} from 'shared/ReactSymbols'; +import {readContext as readContextImpl} from './ReactFlightNewContext'; + +function readContext(context: ReactServerContext): T { + if (__DEV__) { + if (context.$$typeof !== REACT_SERVER_CONTEXT_TYPE) { + console.error('Only ServerContext is supported in Flight'); + } + if (currentCache === null) { + console.error( + 'Context can only be read while React is rendering. ' + + 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + + 'In function components, you can read it directly in the function body, but not ' + + 'inside Hooks like useReducer() or useMemo().', + ); + } + } + return readContextImpl(context); +} + +export const Dispatcher: DispatcherType = { + useMemo(nextCreate: () => T): T { + return nextCreate(); + }, + useCallback(callback: T): T { + return callback; + }, + useDebugValue(): void {}, + useDeferredValue: (unsupportedHook: any), + useTransition: (unsupportedHook: any), + getCacheForType(resourceType: () => T): T { + if (!currentCache) { + throw new Error('Reading the cache is only supported while rendering.'); + } + + let entry: T | void = (currentCache.get(resourceType): any); + if (entry === undefined) { + entry = resourceType(); + // TODO: Warn if undefined? + currentCache.set(resourceType, entry); + } + return entry; + }, + readContext, + useContext: readContext, + useReducer: (unsupportedHook: any), + useRef: (unsupportedHook: any), + useState: (unsupportedHook: any), + useInsertionEffect: (unsupportedHook: any), + useLayoutEffect: (unsupportedHook: any), + useImperativeHandle: (unsupportedHook: any), + useEffect: (unsupportedHook: any), + useId: (unsupportedHook: any), + useMutableSource: (unsupportedHook: any), + useSyncExternalStore: (unsupportedHook: any), + useCacheRefresh(): (?() => T, ?T) => void { + return unsupportedRefresh; + }, +}; + +function unsupportedHook(): void { + throw new Error('This Hook is not supported in Server Components.'); +} + +function unsupportedRefresh(): void { + if (!currentCache) { + throw new Error( + 'Refreshing the cache is not supported in Server Components.', + ); + } +} + +let currentCache: Map | null = null; + +export function setCurrentCache(cache: Map | null) { + currentCache = cache; + return currentCache; +} + +export function getCurrentCache() { + return currentCache; +} diff --git a/packages/react-server/src/ReactFlightNewContext.js b/packages/react-server/src/ReactFlightNewContext.js new file mode 100644 index 000000000000..3f5abaaf0793 --- /dev/null +++ b/packages/react-server/src/ReactFlightNewContext.js @@ -0,0 +1,269 @@ +/** + * 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. + * + * @flow + */ + +import type { + ReactServerContext, + ServerContextJSONValue, +} from 'shared/ReactTypes'; + +import {REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED} from 'shared/ReactSymbols'; +import {isPrimaryRenderer} from './ReactServerFormatConfig'; + +let rendererSigil; +if (__DEV__) { + // Use this to detect multiple renderers using the same context + rendererSigil = {}; +} + +// Used to store the parent path of all context overrides in a shared linked list. +// Forming a reverse tree. +type ContextNode = { + parent: null | ContextNode, + depth: number, // Short hand to compute the depth of the tree at this node. + context: ReactServerContext, + parentValue: T, + value: T, +}; + +// The structure of a context snapshot is an implementation of this file. +// Currently, it's implemented as tracking the current active node. +export opaque type ContextSnapshot = null | ContextNode; + +export const rootContextSnapshot: ContextSnapshot = null; + +// We assume that this runtime owns the "current" field on all ReactContext instances. +// This global (actually thread local) state represents what state all those "current", +// fields are currently in. +let currentActiveSnapshot: ContextSnapshot = null; + +function popNode(prev: ContextNode): void { + if (isPrimaryRenderer) { + prev.context._currentValue = prev.parentValue; + } else { + prev.context._currentValue2 = prev.parentValue; + } +} + +function pushNode(next: ContextNode): void { + if (isPrimaryRenderer) { + next.context._currentValue = next.value; + } else { + next.context._currentValue2 = next.value; + } +} + +function popToNearestCommonAncestor( + prev: ContextNode, + next: ContextNode, +): void { + if (prev === next) { + // We've found a shared ancestor. We don't need to pop nor reapply this one or anything above. + } else { + popNode(prev); + const parentPrev = prev.parent; + const parentNext = next.parent; + if (parentPrev === null) { + if (parentNext !== null) { + throw new Error( + 'The stacks must reach the root at the same time. This is a bug in React.', + ); + } + } else { + if (parentNext === null) { + throw new Error( + 'The stacks must reach the root at the same time. This is a bug in React.', + ); + } + + popToNearestCommonAncestor(parentPrev, parentNext); + // On the way back, we push the new ones that weren't common. + pushNode(next); + } + } +} + +function popAllPrevious(prev: ContextNode): void { + popNode(prev); + const parentPrev = prev.parent; + if (parentPrev !== null) { + popAllPrevious(parentPrev); + } +} + +function pushAllNext(next: ContextNode): void { + const parentNext = next.parent; + if (parentNext !== null) { + pushAllNext(parentNext); + } + pushNode(next); +} + +function popPreviousToCommonLevel( + prev: ContextNode, + next: ContextNode, +): void { + popNode(prev); + const parentPrev = prev.parent; + + if (parentPrev === null) { + throw new Error( + 'The depth must equal at least at zero before reaching the root. This is a bug in React.', + ); + } + + if (parentPrev.depth === next.depth) { + // We found the same level. Now we just need to find a shared ancestor. + popToNearestCommonAncestor(parentPrev, next); + } else { + // We must still be deeper. + popPreviousToCommonLevel(parentPrev, next); + } +} + +function popNextToCommonLevel( + prev: ContextNode, + next: ContextNode, +): void { + const parentNext = next.parent; + + if (parentNext === null) { + throw new Error( + 'The depth must equal at least at zero before reaching the root. This is a bug in React.', + ); + } + + if (prev.depth === parentNext.depth) { + // We found the same level. Now we just need to find a shared ancestor. + popToNearestCommonAncestor(prev, parentNext); + } else { + // We must still be deeper. + popNextToCommonLevel(prev, parentNext); + } + pushNode(next); +} + +// Perform context switching to the new snapshot. +// To make it cheap to read many contexts, while not suspending, we make the switch eagerly by +// updating all the context's current values. That way reads, always just read the current value. +// At the cost of updating contexts even if they're never read by this subtree. +export function switchContext(newSnapshot: ContextSnapshot): void { + // The basic algorithm we need to do is to pop back any contexts that are no longer on the stack. + // We also need to update any new contexts that are now on the stack with the deepest value. + // The easiest way to update new contexts is to just reapply them in reverse order from the + // perspective of the backpointers. To avoid allocating a lot when switching, we use the stack + // for that. Therefore this algorithm is recursive. + // 1) First we pop which ever snapshot tree was deepest. Popping old contexts as we go. + // 2) Then we find the nearest common ancestor from there. Popping old contexts as we go. + // 3) Then we reapply new contexts on the way back up the stack. + const prev = currentActiveSnapshot; + const next = newSnapshot; + if (prev !== next) { + if (prev === null) { + // $FlowFixMe: This has to be non-null since it's not equal to prev. + pushAllNext(next); + } else if (next === null) { + popAllPrevious(prev); + } else if (prev.depth === next.depth) { + popToNearestCommonAncestor(prev, next); + } else if (prev.depth > next.depth) { + popPreviousToCommonLevel(prev, next); + } else { + popNextToCommonLevel(prev, next); + } + currentActiveSnapshot = next; + } +} + +export function pushProvider( + context: ReactServerContext, + nextValue: T, +): ContextSnapshot { + let prevValue; + if (isPrimaryRenderer) { + prevValue = context._currentValue; + context._currentValue = nextValue; + if (__DEV__) { + if ( + context._currentRenderer !== undefined && + context._currentRenderer !== null && + context._currentRenderer !== rendererSigil + ) { + console.error( + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + } + context._currentRenderer = rendererSigil; + } + } else { + prevValue = context._currentValue2; + context._currentValue2 = nextValue; + if (__DEV__) { + if ( + context._currentRenderer2 !== undefined && + context._currentRenderer2 !== null && + context._currentRenderer2 !== rendererSigil + ) { + console.error( + 'Detected multiple renderers concurrently rendering the ' + + 'same context provider. This is currently unsupported.', + ); + } + context._currentRenderer2 = rendererSigil; + } + } + const prevNode = currentActiveSnapshot; + const newNode: ContextNode = { + parent: prevNode, + depth: prevNode === null ? 0 : prevNode.depth + 1, + context: context, + parentValue: prevValue, + value: nextValue, + }; + currentActiveSnapshot = newNode; + return newNode; +} + +export function popProvider(): ContextSnapshot { + const prevSnapshot = currentActiveSnapshot; + + if (prevSnapshot === null) { + throw new Error( + 'Tried to pop a Context at the root of the app. This is a bug in React.', + ); + } + + if (isPrimaryRenderer) { + const value = prevSnapshot.parentValue; + if (value === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED) { + prevSnapshot.context._currentValue = prevSnapshot.context._defaultValue; + } else { + prevSnapshot.context._currentValue = value; + } + } else { + const value = prevSnapshot.parentValue; + if (value === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED) { + prevSnapshot.context._currentValue2 = prevSnapshot.context._defaultValue; + } else { + prevSnapshot.context._currentValue2 = value; + } + } + return (currentActiveSnapshot = prevSnapshot.parent); +} + +export function getActiveContext(): ContextSnapshot { + return currentActiveSnapshot; +} + +export function readContext(context: ReactServerContext): T { + const value = isPrimaryRenderer + ? context._currentValue + : context._currentValue2; + return value; +} diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 2b0e7304af17..32a08b1eff81 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -7,7 +7,6 @@ * @flow */ -import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes'; import type { Destination, Chunk, @@ -16,6 +15,11 @@ import type { ModuleReference, ModuleKey, } from './ReactFlightServerConfig'; +import type {ContextSnapshot} from './ReactFlightNewContext'; +import type { + ReactProviderType, + ServerContextJSONValue, +} from 'shared/ReactTypes'; import { scheduleWork, @@ -27,6 +31,7 @@ import { closeWithError, processModelChunk, processModuleChunk, + processProviderChunk, processSymbolChunk, processErrorChunk, resolveModuleMetaData, @@ -34,14 +39,25 @@ import { isModuleReference, } from './ReactFlightServerConfig'; +import {Dispatcher, getCurrentCache, setCurrentCache} from './ReactFlightHooks'; +import { + pushProvider, + popProvider, + switchContext, + getActiveContext, + rootContextSnapshot, +} from './ReactFlightNewContext'; + import { REACT_ELEMENT_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_LAZY_TYPE, REACT_MEMO_TYPE, + REACT_PROVIDER_TYPE, } from 'shared/ReactSymbols'; +import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import isArray from 'shared/isArray'; @@ -68,6 +84,7 @@ type Segment = { id: number, model: ReactModel, ping: () => void, + context: ContextSnapshot, }; export type Request = { @@ -84,10 +101,15 @@ export type Request = { completedErrorChunks: Array, writtenSymbols: Map, writtenModules: Map, + writtenProviders: Map, onError: (error: mixed) => void, toJSON: (key: string, value: ReactModel) => ReactJSONValue, }; +export type Options = { + onError?: (error: mixed) => void, +}; + const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; function defaultErrorHandler(error: mixed) { @@ -103,6 +125,7 @@ export function createRequest( model: ReactModel, bundlerConfig: BundlerConfig, onError: void | ((error: mixed) => void), + context?: Array<[string, ServerContextJSONValue]>, ): Request { const pingedSegments = []; const request = { @@ -119,17 +142,27 @@ export function createRequest( completedErrorChunks: [], writtenSymbols: new Map(), writtenModules: new Map(), + writtenProviders: new Map(), onError: onError === undefined ? defaultErrorHandler : onError, toJSON: function(key: string, value: ReactModel): ReactJSONValue { return resolveModelToJSON(request, this, key, value); }, }; request.pendingChunks++; - const rootSegment = createSegment(request, model); + const rootContext = createRootContext(context); + const rootSegment = createSegment(request, model, rootContext); pingedSegments.push(rootSegment); return request; } +function createRootContext( + reqContext?: Array<[string, ServerContextJSONValue]>, +) { + return importServerContexts(reqContext); +} + +const POP = {}; + function attemptResolveElement( type: any, key: null | React$Key, @@ -174,6 +207,30 @@ function attemptResolveElement( case REACT_MEMO_TYPE: { return attemptResolveElement(type.type, key, ref, props); } + case REACT_PROVIDER_TYPE: { + pushProvider(type._context, props.value); + if (__DEV__) { + const extraKeys = Object.keys(props).filter(value => { + if (value === 'children' || value === 'value') { + return false; + } + return true; + }); + if (extraKeys.length !== 0) { + console.error( + 'ServerContext can only have a value prop and children. Found: %s', + JSON.stringify(extraKeys), + ); + } + } + return [ + REACT_ELEMENT_TYPE, + type, + key, + // Rely on __popProvider being serialized last to pop the provider. + {value: props.value, children: props.children, __pop: POP}, + ]; + } } } throw new Error( @@ -189,11 +246,16 @@ function pingSegment(request: Request, segment: Segment): void { } } -function createSegment(request: Request, model: ReactModel): Segment { +function createSegment( + request: Request, + model: ReactModel, + context: ContextSnapshot, +): Segment { const id = request.nextChunkId++; const segment = { id, model, + context, ping: () => pingSegment(request, segment), }; return segment; @@ -221,7 +283,6 @@ function isObjectPrototype(object): boolean { if (!object) { return false; } - // $FlowFixMe const ObjectPrototype = Object.prototype; if (object === ObjectPrototype) { return true; @@ -311,7 +372,6 @@ function describeObjectForErrorMessage( ): string { if (isArray(objectOrArray)) { let str = '['; - // $FlowFixMe: Should be refined by now. const array: $ReadOnlyArray = objectOrArray; for (let i = 0; i < array.length; i++) { if (i > 0) { @@ -336,7 +396,6 @@ function describeObjectForErrorMessage( return str; } else { let str = '{'; - // $FlowFixMe: Should be refined by now. const object: {+[key: string | number]: ReactModel} = objectOrArray; const names = Object.keys(object); for (let i = 0; i < names.length; i++) { @@ -365,6 +424,9 @@ function describeObjectForErrorMessage( } } +let insideContextProps = null; +let isInsideContextValue = false; + export function resolveModelToJSON( request: Request, parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray, @@ -396,12 +458,32 @@ export function resolveModelToJSON( ); } + if (__DEV__) { + if ( + parent[0] === REACT_ELEMENT_TYPE && + parent[1] && + parent[1].$$typeof === REACT_PROVIDER_TYPE && + key === '3' + ) { + insideContextProps = value; + } else if (insideContextProps === parent && key === 'value') { + isInsideContextValue = true; + } else if (insideContextProps === parent && key === 'children') { + isInsideContextValue = false; + } + } + // Resolve server components. while ( typeof value === 'object' && value !== null && - value.$$typeof === REACT_ELEMENT_TYPE + (value: any).$$typeof === REACT_ELEMENT_TYPE ) { + if (__DEV__) { + if (isInsideContextValue) { + console.error('React elements are not allowed in ServerContext'); + } + } // TODO: Concatenate keys of parents onto children. const element: React$Element = (value: any); try { @@ -416,7 +498,7 @@ export function resolveModelToJSON( if (typeof x === 'object' && x !== null && typeof x.then === 'function') { // Something suspended, we'll need to create a new segment and resolve it later. request.pendingChunks++; - const newSegment = createSegment(request, value); + const newSegment = createSegment(request, value, getActiveContext()); const ping = newSegment.ping; x.then(ping, ping); return serializeByRefID(newSegment.id); @@ -478,6 +560,25 @@ export function resolveModelToJSON( emitErrorChunk(request, errorId, x); return serializeByValueID(errorId); } + } else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) { + const providerKey = ((value: any): ReactProviderType)._context + ._globalName; + const writtenProviders = request.writtenProviders; + let providerId = writtenProviders.get(key); + if (providerId === undefined) { + request.pendingChunks++; + providerId = request.nextChunkId++; + writtenProviders.set(providerKey, providerId); + emitProviderChunk(request, providerId, providerKey); + } + return serializeByValueID(providerId); + } else if (value === POP) { + popProvider(); + if (__DEV__) { + insideContextProps = null; + isInsideContextValue = false; + } + return (undefined: any); } if (__DEV__) { @@ -515,6 +616,7 @@ export function resolveModelToJSON( } } } + return value; } @@ -657,13 +759,23 @@ function emitSymbolChunk(request: Request, id: number, name: string): void { request.completedModuleChunks.push(processedChunk); } +function emitProviderChunk( + request: Request, + id: number, + contextName: string, +): void { + const processedChunk = processProviderChunk(request, id, contextName); + request.completedJSONChunks.push(processedChunk); +} + function retrySegment(request: Request, segment: Segment): void { + switchContext(segment.context); try { let value = segment.model; while ( typeof value === 'object' && value !== null && - value.$$typeof === REACT_ELEMENT_TYPE + (value: any).$$typeof === REACT_ELEMENT_TYPE ) { // TODO: Concatenate keys of parents onto children. const element: React$Element = (value: any); @@ -696,9 +808,9 @@ function retrySegment(request: Request, segment: Segment): void { function performWork(request: Request): void { const prevDispatcher = ReactCurrentDispatcher.current; - const prevCache = currentCache; + const prevCache = getCurrentCache(); ReactCurrentDispatcher.current = Dispatcher; - currentCache = request.cache; + setCurrentCache(request.cache); try { const pingedSegments = request.pingedSegments; @@ -715,7 +827,7 @@ function performWork(request: Request): void { fatalError(request, error); } finally { ReactCurrentDispatcher.current = prevDispatcher; - currentCache = prevCache; + setCurrentCache(prevCache); } } @@ -806,56 +918,20 @@ export function startFlowing(request: Request, destination: Destination): void { } } -function unsupportedHook(): void { - throw new Error('This Hook is not supported in Server Components.'); -} - -function unsupportedRefresh(): void { - if (!currentCache) { - throw new Error( - 'Refreshing the cache is not supported in Server Components.', - ); +function importServerContexts( + contexts?: Array<[string, ServerContextJSONValue]>, +) { + if (contexts) { + const prevContext = getActiveContext(); + switchContext(rootContextSnapshot); + for (let i = 0; i < contexts.length; i++) { + const [name, value] = contexts[i]; + const context = getOrCreateServerContext(name); + pushProvider(context, value); + } + const importedContext = getActiveContext(); + switchContext(prevContext); + return importedContext; } + return rootContextSnapshot; } - -let currentCache: Map | null = null; - -const Dispatcher: DispatcherType = { - useMemo(nextCreate: () => T): T { - return nextCreate(); - }, - useCallback(callback: T): T { - return callback; - }, - useDebugValue(): void {}, - useDeferredValue: (unsupportedHook: any), - useTransition: (unsupportedHook: any), - getCacheForType(resourceType: () => T): T { - if (!currentCache) { - throw new Error('Reading the cache is only supported while rendering.'); - } - - let entry: T | void = (currentCache.get(resourceType): any); - if (entry === undefined) { - entry = resourceType(); - // TODO: Warn if undefined? - currentCache.set(resourceType, entry); - } - return entry; - }, - readContext: (unsupportedHook: any), - useContext: (unsupportedHook: any), - useReducer: (unsupportedHook: any), - useRef: (unsupportedHook: any), - useState: (unsupportedHook: any), - useInsertionEffect: (unsupportedHook: any), - useLayoutEffect: (unsupportedHook: any), - useImperativeHandle: (unsupportedHook: any), - useEffect: (unsupportedHook: any), - useId: (unsupportedHook: any), - useMutableSource: (unsupportedHook: any), - useSyncExternalStore: (unsupportedHook: any), - useCacheRefresh(): (?() => T, ?T) => void { - return unsupportedRefresh; - }, -}; diff --git a/packages/react-server/src/ReactFlightServerConfigStream.js b/packages/react-server/src/ReactFlightServerConfigStream.js index 74a90f7a02a7..08e9cbff2f50 100644 --- a/packages/react-server/src/ReactFlightServerConfigStream.js +++ b/packages/react-server/src/ReactFlightServerConfigStream.js @@ -109,6 +109,15 @@ export function processModuleChunk( return stringToChunk(row); } +export function processProviderChunk( + request: Request, + id: number, + contextName: string, +): Chunk { + const row = serializeRowHeader('P', id) + contextName + '\n'; + return stringToChunk(row); +} + export function processSymbolChunk( request: Request, id: number, diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 33c770f00fed..76326a0fe59d 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -26,6 +26,7 @@ export { createMutableSource, createMutableSource as unstable_createMutableSource, createRef, + createServerContext, forwardRef, isValidElement, lazy, @@ -53,10 +54,10 @@ export { useMemo, useMutableSource, useMutableSource as unstable_useMutableSource, - useSyncExternalStore, useReducer, useRef, useState, + useSyncExternalStore, useTransition, useTransition as unstable_useTransition, // TODO: Remove once call sights updated to useTransition version, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index d4dc33a4db01..19af075993d5 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -24,6 +24,7 @@ export { createFactory, createMutableSource as unstable_createMutableSource, createRef, + createServerContext, forwardRef, isValidElement, lazy, @@ -46,10 +47,10 @@ export { useInsertionEffect, useLayoutEffect, useMemo, - useSyncExternalStore, useReducer, useRef, useState, + useSyncExternalStore, useTransition, version, } from './src/React'; diff --git a/packages/react/index.js b/packages/react/index.js index e4946bf095b6..084aabb53c6b 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -48,6 +48,7 @@ export { createFactory, createMutableSource, createRef, + createServerContext, forwardRef, isValidElement, lazy, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 5b0d75e21460..e9f80ade0610 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -25,6 +25,7 @@ export { createMutableSource, createMutableSource as unstable_createMutableSource, createRef, + createServerContext, forwardRef, isValidElement, lazy, @@ -52,10 +53,10 @@ export { useMemo, useMutableSource, useMutableSource as unstable_useMutableSource, - useSyncExternalStore, useReducer, useRef, useState, + useSyncExternalStore, useTransition, useTransition as unstable_useTransition, // TODO: Remove once call sights updated to useTransition version, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 53497831e004..3ed868197b6f 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -37,10 +37,10 @@ export { useInsertionEffect, useLayoutEffect, useMemo, - useSyncExternalStore, useReducer, useRef, useState, + useSyncExternalStore, useTransition, version, } from './src/React'; diff --git a/packages/react/src/React.js b/packages/react/src/React.js index b899f51c80b6..264c1e1dc56d 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -61,6 +61,7 @@ import { createFactoryWithValidation, cloneElementWithValidation, } from './ReactElementValidator'; +import {createServerContext} from './ReactServerContext'; import {createMutableSource} from './ReactMutableSource'; import ReactSharedInternals from './ReactSharedInternals'; import {startTransition} from './ReactStartTransition'; @@ -86,6 +87,7 @@ export { Component, PureComponent, createContext, + createServerContext, forwardRef, lazy, memo, diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 41065c13ef06..e547a411009f 100644 --- a/packages/react/src/ReactContext.js +++ b/packages/react/src/ReactContext.js @@ -30,6 +30,10 @@ export function createContext(defaultValue: T): ReactContext { // These are circular Provider: (null: any), Consumer: (null: any), + + // Add these to use same hidden class in VM as ServerContext + _defaultValue: (null: any), + _globalName: (null: any), }; context.Provider = { diff --git a/packages/react/src/ReactServerContext.js b/packages/react/src/ReactServerContext.js new file mode 100644 index 000000000000..a561fbb92e79 --- /dev/null +++ b/packages/react/src/ReactServerContext.js @@ -0,0 +1,104 @@ +/** + * 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. + * + * @flow + */ + +import { + REACT_PROVIDER_TYPE, + REACT_SERVER_CONTEXT_TYPE, + REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED, +} from 'shared/ReactSymbols'; + +import type { + ReactServerContext, + ServerContextJSONValue, +} from 'shared/ReactTypes'; + +import {enableServerContext} from 'shared/ReactFeatureFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; + +const ContextRegistry = ReactSharedInternals.ContextRegistry; + +export function createServerContext( + globalName: string, + defaultValue: T, +): ReactServerContext { + if (!enableServerContext) { + throw new Error('Not implemented.'); + } + let wasDefined = true; + if (!ContextRegistry[globalName]) { + wasDefined = false; + const context: ReactServerContext = { + $$typeof: REACT_SERVER_CONTEXT_TYPE, + + // As a workaround to support multiple concurrent renderers, we categorize + // some renderers as primary and others as secondary. We only expect + // there to be two concurrent renderers at most: React Native (primary) and + // Fabric (secondary); React DOM (primary) and React ART (secondary). + // Secondary renderers store their context values on separate fields. + _currentValue: defaultValue, + _currentValue2: defaultValue, + + _defaultValue: defaultValue, + + // Used to track how many concurrent renderers this context currently + // supports within in a single renderer. Such as parallel server rendering. + _threadCount: 0, + // These are circular + Provider: (null: any), + Consumer: (null: any), + _globalName: globalName, + }; + + context.Provider = { + $$typeof: REACT_PROVIDER_TYPE, + _context: context, + }; + + if (__DEV__) { + let hasWarnedAboutUsingConsumer; + context._currentRenderer = null; + context._currentRenderer2 = null; + Object.defineProperties( + context, + ({ + Consumer: { + get() { + if (!hasWarnedAboutUsingConsumer) { + console.error( + 'Consumer pattern is not supported by ReactServerContext', + ); + hasWarnedAboutUsingConsumer = true; + } + return null; + }, + }, + }: any), + ); + } + ContextRegistry[globalName] = context; + } + + const context = ContextRegistry[globalName]; + if (context._defaultValue === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED) { + context._defaultValue = defaultValue; + if ( + context._currentValue === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED + ) { + context._currentValue = defaultValue; + } + if ( + context._currentValue2 === REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED + ) { + context._currentValue2 = defaultValue; + } + } else if (wasDefined) { + throw new Error(`ServerContext: ${globalName} already defined`); + } + return context; +} diff --git a/packages/react/src/ReactServerContextRegistry.js b/packages/react/src/ReactServerContextRegistry.js new file mode 100644 index 000000000000..dda738ac7439 --- /dev/null +++ b/packages/react/src/ReactServerContextRegistry.js @@ -0,0 +1,5 @@ +import type {ReactServerContext} from 'shared/ReactTypes'; + +export const ContextRegistry: { + [globalName: string]: ReactServerContext, +} = {}; diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js index 2874b03985b9..6f160b96ec8b 100644 --- a/packages/react/src/ReactSharedInternals.js +++ b/packages/react/src/ReactSharedInternals.js @@ -10,6 +10,8 @@ import ReactCurrentBatchConfig from './ReactCurrentBatchConfig'; import ReactCurrentActQueue from './ReactCurrentActQueue'; import ReactCurrentOwner from './ReactCurrentOwner'; import ReactDebugCurrentFrame from './ReactDebugCurrentFrame'; +import {enableServerContext} from 'shared/ReactFeatureFlags'; +import {ContextRegistry} from './ReactServerContextRegistry'; const ReactSharedInternals = { ReactCurrentDispatcher, @@ -22,4 +24,8 @@ if (__DEV__) { ReactSharedInternals.ReactCurrentActQueue = ReactCurrentActQueue; } +if (enableServerContext) { + ReactSharedInternals.ContextRegistry = ContextRegistry; +} + export default ReactSharedInternals; diff --git a/packages/react/src/forks/ReactSharedInternals.umd.js b/packages/react/src/forks/ReactSharedInternals.umd.js index 04e8cb577f7a..57e96e665477 100644 --- a/packages/react/src/forks/ReactSharedInternals.umd.js +++ b/packages/react/src/forks/ReactSharedInternals.umd.js @@ -11,6 +11,8 @@ import ReactCurrentActQueue from '../ReactCurrentActQueue'; import ReactCurrentOwner from '../ReactCurrentOwner'; import ReactDebugCurrentFrame from '../ReactDebugCurrentFrame'; import ReactCurrentBatchConfig from '../ReactCurrentBatchConfig'; +import {enableServerContext} from 'shared/ReactFeatureFlags'; +import {ContextRegistry} from '../ReactServerContextRegistry'; const ReactSharedInternals = { ReactCurrentDispatcher, @@ -30,4 +32,8 @@ if (__DEV__) { ReactSharedInternals.ReactDebugCurrentFrame = ReactDebugCurrentFrame; } +if (enableServerContext) { + ReactSharedInternals.ContextRegistry = ContextRegistry; +} + export default ReactSharedInternals; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index e60f2db9f40b..7cd1325c5789 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -249,6 +249,7 @@ export const enableUpdaterTracking = __PROFILE__; // Only enabled in RN, related to enableComponentStackLocations export const disableNativeComponentFrames = false; +export const enableServerContext = __EXPERIMENTAL__; // Internal only. export const enableGetInspectorDataForInstanceInProduction = false; diff --git a/packages/shared/ReactServerContextRegistry.js b/packages/shared/ReactServerContextRegistry.js new file mode 100644 index 000000000000..ab8421b3c329 --- /dev/null +++ b/packages/shared/ReactServerContextRegistry.js @@ -0,0 +1,24 @@ +/** + * 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. + * + * @flow + */ + +import {REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED} from 'shared/ReactSymbols'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import {createServerContext} from 'react'; + +const ContextRegistry = ReactSharedInternals.ContextRegistry; + +export function getOrCreateServerContext(globalName: string) { + if (!ContextRegistry[globalName]) { + ContextRegistry[globalName] = createServerContext( + globalName, + REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED, + ); + } + return ContextRegistry[globalName]; +} diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 299869d12e8a..6ff9305fa7b0 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -19,6 +19,7 @@ export const REACT_STRICT_MODE_TYPE = Symbol.for('react.strict_mode'); export const REACT_PROFILER_TYPE = Symbol.for('react.profiler'); export const REACT_PROVIDER_TYPE = Symbol.for('react.provider'); export const REACT_CONTEXT_TYPE = Symbol.for('react.context'); +export const REACT_SERVER_CONTEXT_TYPE = Symbol.for('react.server_context'); export const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref'); export const REACT_SUSPENSE_TYPE = Symbol.for('react.suspense'); export const REACT_SUSPENSE_LIST_TYPE = Symbol.for('react.suspense_list'); @@ -32,6 +33,9 @@ export const REACT_OFFSCREEN_TYPE = Symbol.for('react.offscreen'); export const REACT_LEGACY_HIDDEN_TYPE = Symbol.for('react.legacy_hidden'); export const REACT_CACHE_TYPE = Symbol.for('react.cache'); export const REACT_TRACING_MARKER_TYPE = Symbol.for('react.tracing_marker'); +export const REACT_SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED = Symbol.for( + 'react.default_value', +); const MAYBE_ITERATOR_SYMBOL = Symbol.iterator; const FAUX_ITERATOR_SYMBOL = '@@iterator'; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 066d20552d6f..17aa509e89eb 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -67,9 +67,23 @@ export type ReactContext = { // This value may be added by application code // to improve DEV tooling display names displayName?: string, + + // only used by ServerContext + _defaultValue: T, + _globalName: string, ... }; +export type ServerContextJSONValue = + | string + | boolean + | number + | null + | $ReadOnlyArray + | {+[key: string]: ServerContextJSONValue}; + +export type ReactServerContext = ReactContext; + export type ReactPortal = { $$typeof: Symbol | number, key: null | string, diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 01a036bb6036..8decbc01d680 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -75,6 +75,8 @@ export const allowConcurrentByDefault = true; export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableServerContext = false; + export const enableUseMutableSource = true; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index aa44bfafbf79..3ca6029d6785 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -67,6 +67,7 @@ export const enablePersistentOffscreenHostContainer = false; export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableServerContext = false; export const enableUseMutableSource = false; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 2a39b7d17e53..ca9f71857c02 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -67,6 +67,7 @@ export const enablePersistentOffscreenHostContainer = false; export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableServerContext = false; export const enableUseMutableSource = false; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index ceb8fb196e90..b2afb7982547 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -65,6 +65,7 @@ export const allowConcurrentByDefault = true; export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableServerContext = false; export const enableUseMutableSource = false; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 804242d8094d..7f776d1d40f1 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -67,6 +67,7 @@ export const enablePersistentOffscreenHostContainer = false; export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableServerContext = false; // Some www surfaces are still using this. Remove once they have been migrated. export const enableUseMutableSource = true; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 27a37f53e990..30346c8c62ce 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -67,6 +67,7 @@ export const enablePersistentOffscreenHostContainer = false; export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; +export const enableServerContext = false; export const enableUseMutableSource = false; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 6a34c1aa3b3d..e75cbaeb5a4a 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -67,7 +67,7 @@ export const enablePersistentOffscreenHostContainer = false; export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; - +export const enableServerContext = false; // Some www surfaces are still using this. Remove once they have been migrated. export const enableUseMutableSource = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 99b68fc7562c..91a906d90bda 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -102,6 +102,7 @@ export const deletedTreeCleanUpLevel = 3; export const enablePersistentOffscreenHostContainer = false; export const consoleManagedByDevToolsDuringStrictMode = true; +export const enableServerContext = true; // Some www surfaces are still using this. Remove once they have been migrated. export const enableUseMutableSource = true; diff --git a/packages/shared/getComponentNameFromType.js b/packages/shared/getComponentNameFromType.js index 7a732d915c21..91430e225718 100644 --- a/packages/shared/getComponentNameFromType.js +++ b/packages/shared/getComponentNameFromType.js @@ -24,9 +24,14 @@ import { REACT_LAZY_TYPE, REACT_CACHE_TYPE, REACT_TRACING_MARKER_TYPE, + REACT_SERVER_CONTEXT_TYPE, } from 'shared/ReactSymbols'; -import {enableTransitionTracing, enableCache} from './ReactFeatureFlags'; +import { + enableServerContext, + enableTransitionTracing, + enableCache, +} from './ReactFeatureFlags'; // Keep in sync with react-reconciler/getComponentNameFromFiber function getWrappedName( @@ -116,6 +121,12 @@ export default function getComponentNameFromType(type: mixed): string | null { return null; } } + case REACT_SERVER_CONTEXT_TYPE: + if (enableServerContext) { + const context2 = ((type: any): ReactContext); + return (context2.displayName || context2._globalName) + '.Provider'; + } + // eslint-disable-next-line no-fallthrough } } return null; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 367b736931cf..92d3bfd128c8 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -405,10 +405,16 @@ "417": "React currently only supports piping to one writable stream.", "418": "Hydration failed because the initial UI does not match what was rendered on the server.", "419": "The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.", - "420": "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.", - "421": "There was an error while hydrating this Suspense boundary. Switched to client rendering.", - "422": "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.", - "423": "This root received an early update, before anything was able hydrate. Switched the entire root to client rendering.", - "424": "Text content does not match server-rendered HTML.", - "425": "A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition." + "420": "ServerContext: %s already defined", + "421": "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.", + "422": "There was an error while hydrating this Suspense boundary. Switched to client rendering.", + "423": "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.", + "424": "This root received an early update, before anything was able hydrate. Switched the entire root to client rendering.", + "425": "Text content does not match server-rendered HTML.", + "426": "A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.", + "427": "useServerContext expects a context created with React.createServerContext", + "428": "useServerContext is only supported while rendering.", + "429": "ServerContext: %s already defined", + "430": "ServerContext can only have a value prop and children. Found: %s", + "431": "React elements are not allowed in ServerContext" }