From 3e00319b352092949ba146e70829310fe6362062 Mon Sep 17 00:00:00 2001 From: Ricky Date: Tue, 3 Feb 2026 10:22:57 -0500 Subject: [PATCH] [Flight] allow context providers from client modules (#35675) Allows Server Components to import Context from a `"use client'` module and render its Provider. Only tricky part was that I needed to add `REACT_CONTEXT_TYPE` handling in mountLazyComponent so lazy-resolved Context types can be rendered. Previously only functions, REACT_FORWARD_REF_TYPE, and REACT_MEMO_TYPE were handled. Tested in the Flight fixture. ty bb claude Closes https://github.com/facebook/react/issues/35340 --------- Co-authored-by: Sophie Alpert --- fixtures/flight/src/App.js | 7 +- fixtures/flight/src/ClientContext.js | 12 ++++ .../src/ReactFiberBeginWork.js | 5 ++ .../src/__tests__/ReactLazy-test.internal.js | 66 +++++++++++++++---- .../src/ReactFlightTurbopackReferences.js | 9 ++- .../src/ReactFlightUnbundledReferences.js | 9 ++- .../src/ReactFlightWebpackReferences.js | 9 ++- .../src/__tests__/ReactFlightDOM-test.js | 59 +++++++++++++++-- .../ReactFlightServerTemporaryReferences.js | 9 ++- 9 files changed, 145 insertions(+), 40 deletions(-) create mode 100644 fixtures/flight/src/ClientContext.js diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index 0bfe0fe630..9f8deed495 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -2,7 +2,7 @@ import * as React from 'react'; import {renderToReadableStream} from 'react-server-dom-unbundled/server'; import {createFromReadableStream} from 'react-server-dom-webpack/client'; import {PassThrough, Readable} from 'stream'; - +import {ClientContext, ClientReadContext} from './ClientContext.js'; import Container from './Container.js'; import {Counter} from './Counter.js'; @@ -235,6 +235,11 @@ export default async function App({prerender, noCache}) { {dedupedChild} {Promise.resolve([dedupedChild])} + +
+ +
+
{prerender ? null : ( // TODO: prerender is broken for large content for some reason. diff --git a/fixtures/flight/src/ClientContext.js b/fixtures/flight/src/ClientContext.js new file mode 100644 index 0000000000..7d9340e62a --- /dev/null +++ b/fixtures/flight/src/ClientContext.js @@ -0,0 +1,12 @@ +'use client'; + +import {createContext, use} from 'react'; + +const ClientContext = createContext(null); + +function ClientReadContext() { + const value = use(ClientContext); + return

{value}

; +} + +export {ClientContext, ClientReadContext}; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 4ea1a15263..48ced58873 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -128,6 +128,7 @@ import { REACT_LAZY_TYPE, REACT_FORWARD_REF_TYPE, REACT_MEMO_TYPE, + REACT_CONTEXT_TYPE, } from 'shared/ReactSymbols'; import {setCurrentFiber} from './ReactCurrentFiber'; import { @@ -2140,6 +2141,10 @@ function mountLazyComponent( props, renderLanes, ); + } else if ($$typeof === REACT_CONTEXT_TYPE) { + workInProgress.tag = ContextProvider; + workInProgress.type = Component; + return updateContextProvider(null, workInProgress, renderLanes); } } diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index 54e6c1aabf..0cb58d84a7 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -116,6 +116,44 @@ describe('ReactLazy', () => { expect(root).toMatchRenderedOutput('Hi again'); }); + it('renders a lazy context provider', async () => { + const Context = React.createContext('default'); + function ConsumerText() { + return ; + } + // Context.Provider === Context, so we can lazy-load the context itself + const LazyProvider = lazy(() => fakeImport(Context)); + + const root = ReactTestRenderer.create( + }> + + + + , + { + unstable_isConcurrent: true, + }, + ); + + await waitForAll(['Loading...']); + expect(root).not.toMatchRenderedOutput('Hi'); + + await act(() => resolveFakeImport(Context)); + assertLog(['Hi']); + expect(root).toMatchRenderedOutput('Hi'); + + // Should not suspend on update + root.update( + }> + + + + , + ); + await waitForAll(['Hi again']); + expect(root).toMatchRenderedOutput('Hi again'); + }); + it('can resolve synchronously without suspending', async () => { const LazyText = lazy(() => ({ then(cb) { @@ -858,13 +896,20 @@ describe('ReactLazy', () => { ); }); - it('throws with a useful error when wrapping Context with lazy()', async () => { - const Context = React.createContext(null); - const BadLazy = lazy(() => fakeImport(Context)); + it('renders a lazy context provider without value prop', async () => { + // Context providers work when wrapped in lazy() + const Context = React.createContext('default'); + const LazyProvider = lazy(() => fakeImport(Context)); + + function ConsumerText() { + return ; + } const root = ReactTestRenderer.create( }> - + + + , { unstable_isConcurrent: true, @@ -873,16 +918,9 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); - await resolveFakeImport(Context); - root.update( - }> - - , - ); - await waitForThrow( - 'Element type is invalid. Received a promise that resolves to: Context. ' + - 'Lazy element type must resolve to a class or function.', - ); + await act(() => resolveFakeImport(Context)); + assertLog(['provided']); + expect(root).toMatchRenderedOutput('provided'); }); it('throws with a useful error when wrapping Context.Consumer with lazy()', async () => { diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js index 082a9c0ce5..50f83d31d8 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js @@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps = { // $FlowFixMe[prop-missing] return Object.prototype[Symbol.toStringTag]; case 'Provider': - throw new Error( - `Cannot render a Client Context Provider on the Server. ` + - `Instead, you can export a Client Component wrapper ` + - `that itself renders a Client Context Provider.`, - ); + // Context.Provider === Context in React, so return the same reference. + // This allows server components to render + // which will be serialized and executed on the client. + return receiver; case 'then': throw new Error( `Cannot await or return from a thenable. ` + diff --git a/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js index 07646e18ec..b9f90b4d14 100644 --- a/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js +++ b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js @@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps = { // $FlowFixMe[prop-missing] return Object.prototype[Symbol.toStringTag]; case 'Provider': - throw new Error( - `Cannot render a Client Context Provider on the Server. ` + - `Instead, you can export a Client Component wrapper ` + - `that itself renders a Client Context Provider.`, - ); + // Context.Provider === Context in React, so return the same reference. + // This allows server components to render + // which will be serialized and executed on the client. + return receiver; case 'then': throw new Error( `Cannot await or return from a thenable. ` + diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js index 07646e18ec..b9f90b4d14 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js @@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps = { // $FlowFixMe[prop-missing] return Object.prototype[Symbol.toStringTag]; case 'Provider': - throw new Error( - `Cannot render a Client Context Provider on the Server. ` + - `Instead, you can export a Client Component wrapper ` + - `that itself renders a Client Context Provider.`, - ); + // Context.Provider === Context in React, so return the same reference. + // This allows server components to render + // which will be serialized and executed on the client. + return receiver; case 'then': throw new Error( `Cannot await or return from a thenable. ` + diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 728b8ac197..7070f39cea 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -787,7 +787,7 @@ describe('ReactFlightDOM', () => { ; }); - it('throws when accessing a Context.Provider below the client exports', () => { + it('does not throw when accessing a Context.Provider from client exports', () => { const Context = React.createContext(); const ClientModule = clientExports({ Context, @@ -795,11 +795,60 @@ describe('ReactFlightDOM', () => { function dotting() { return ClientModule.Context.Provider; } - expect(dotting).toThrowError( - `Cannot render a Client Context Provider on the Server. ` + - `Instead, you can export a Client Component wrapper ` + - `that itself renders a Client Context Provider.`, + expect(dotting).not.toThrowError(); + }); + + it('can render a client Context.Provider from a server component', async () => { + // Create a context in a client module + const TestContext = React.createContext('default'); + const ClientModule = clientExports({ + TestContext, + }); + + // Client component that reads context + function ClientConsumer() { + const value = React.useContext(TestContext); + return {value}; + } + const {ClientConsumer: ClientConsumerRef} = clientExports({ClientConsumer}); + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + // Server component that provides context + function ServerApp() { + return ( + +
+ +
+
+ ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = await serverAct(() => + ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + expect(container.innerHTML).toBe('
from-server
'); }); it('should progressively reveal server components', async () => { diff --git a/packages/react-server/src/ReactFlightServerTemporaryReferences.js b/packages/react-server/src/ReactFlightServerTemporaryReferences.js index 581fdc70fe..9195afa494 100644 --- a/packages/react-server/src/ReactFlightServerTemporaryReferences.js +++ b/packages/react-server/src/ReactFlightServerTemporaryReferences.js @@ -65,11 +65,10 @@ const proxyHandlers: Proxy$traps = { // $FlowFixMe[prop-missing] return Object.prototype[Symbol.toStringTag]; case 'Provider': - throw new Error( - `Cannot render a Client Context Provider on the Server. ` + - `Instead, you can export a Client Component wrapper ` + - `that itself renders a Client Context Provider.`, - ); + // Context.Provider === Context in React, so return the same reference. + // This allows server components to render + // which will be serialized and executed on the client. + return receiver; case 'then': // Allow returning a temporary reference from an async function // Unlike regular Client References, a Promise would never have been serialized as