diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 9d5ac3680a8a0..dabb215c708f6 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 9f07d8cc999ad..8af1734de6b71 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 8d0fbe1609d98..9197a4c3bfe5c 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 ebc6920be8d2f..775ad34e77207 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 88c1d53608372..665b85d183a3f 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 0aacf5a755247..06e83ba128be3 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 dd81ec036158d..8c5d26a08c2ae 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 ca6fedf2ec63b..1c607befe74b8 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 14ec05bd5b460..67588c3219a92 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 a34de4142e4ce..e530619d41664 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 011c64f59d2c8..d1c50b42d8cbc 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 48f8f14bf2e2a..137ee33cba512 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 8ff30c810f03f..8f269a4050215 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 93fe3bc8395c7..a48c842043821 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 162ba457d5490..dd2e09c03b210 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 94f73b0d7a4ac..60d19ecef3f78 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 971eca0908cb6..a29a6449d8b4d 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 ababa89250e3c..0a8c3389de711 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 35518c9d33bf2..aeee2d24806db 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 5f992d4b03e9b..c088725f25909 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 241913f91e6a7..e97b2f9b2be77 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 75f6db8039ab0..1c32ac0dd4d44 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 0387d94ecad28..b07c084eaa691 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 0eaa07f839a16..b4386399c3340 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 0000000000000..88a2eac86ca2b --- /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 0000000000000..3f5abaaf0793f --- /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 2b0e7304af171..32a08b1eff812 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 74a90f7a02a7e..08e9cbff2f508 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 33c770f00fed5..76326a0fe59d0 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 d4dc33a4db01a..19af075993d56 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 e4946bf095b69..084aabb53c6bf 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 5b0d75e21460b..e9f80ade06105 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 53497831e004d..3ed868197b6f8 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 b899f51c80b6e..264c1e1dc56d0 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 41065c13ef067..e547a411009ff 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 0000000000000..a561fbb92e795 --- /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 0000000000000..dda738ac7439c --- /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 2874b03985b9e..6f160b96ec8be 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 04e8cb577f7ab..57e96e6654770 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 e60f2db9f40bc..7cd1325c57890 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 0000000000000..ab8421b3c329a --- /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 299869d12e8ab..6ff9305fa7b00 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 066d20552d6fc..17aa509e89eb9 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 01a036bb6036d..8decbc01d6802 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 aa44bfafbf79b..3ca6029d67852 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 2a39b7d17e532..ca9f71857c02f 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 ceb8fb196e90f..b2afb79825479 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 804242d8094de..7f776d1d40f1a 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 27a37f53e9900..30346c8c62cef 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 6a34c1aa3b3d0..e75cbaeb5a4ae 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 99b68fc7562c7..91a906d90bdac 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 7a732d915c210..91430e2257184 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 367b736931cfc..92d3bfd128c8a 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" }