@samitouri / QOS-React-1 / commits / 3e00319b35

[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 <git@sophiebits.com>

Ricky committed Feb 3, 2026 at 10:22 UTC 3e00319b352092949ba146e70829310fe6362062
9 files changed +145 -40
fixtures/flight/src/App.js
+6 -1
@@ -2,7 +2,7 @@ import * as React from 'react';
2 import {renderToReadableStream} from 'react-server-dom-unbundled/server';
3 import {createFromReadableStream} from 'react-server-dom-webpack/client';
4 import {PassThrough, Readable} from 'stream';
5 -
5 +import {ClientContext, ClientReadContext} from './ClientContext.js';
6 import Container from './Container.js';
7
8 import {Counter} from './Counter.js';
@@ -235,6 +235,11 @@ export default async function App({prerender, noCache}) {
235 <Foo>{dedupedChild}</Foo>
236 <Bar>{Promise.resolve([dedupedChild])}</Bar>
237 <Navigate />
238 + <ClientContext value="from server">
239 + <div>
240 + <ClientReadContext />
241 + </div>
242 + </ClientContext>
243 {prerender ? null : ( // TODO: prerender is broken for large content for some reason.
244 <React.Suspense fallback={null}>
245 <LargeContent />
fixtures/flight/src/ClientContext.js new
+12
@@ -0,0 +1,12 @@
1 +'use client';
2 +
3 +import {createContext, use} from 'react';
4 +
5 +const ClientContext = createContext(null);
6 +
7 +function ClientReadContext() {
8 + const value = use(ClientContext);
9 + return <p>{value}</p>;
10 +}
11 +
12 +export {ClientContext, ClientReadContext};
packages/react-reconciler/src/ReactFiberBeginWork.js
+5
@@ -128,6 +128,7 @@ import {
128 REACT_LAZY_TYPE,
129 REACT_FORWARD_REF_TYPE,
130 REACT_MEMO_TYPE,
131 + REACT_CONTEXT_TYPE,
132 } from 'shared/ReactSymbols';
133 import {setCurrentFiber} from './ReactCurrentFiber';
134 import {
@@ -2140,6 +2141,10 @@ function mountLazyComponent(
2141 props,
2142 renderLanes,
2143 );
2144 + } else if ($$typeof === REACT_CONTEXT_TYPE) {
2145 + workInProgress.tag = ContextProvider;
2146 + workInProgress.type = Component;
2147 + return updateContextProvider(null, workInProgress, renderLanes);
2148 }
2149 }
2150
packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+52 -14
@@ -116,6 +116,44 @@ describe('ReactLazy', () => {
116 expect(root).toMatchRenderedOutput('Hi again');
117 });
118
119 + it('renders a lazy context provider', async () => {
120 + const Context = React.createContext('default');
121 + function ConsumerText() {
122 + return <Text text={React.useContext(Context)} />;
123 + }
124 + // Context.Provider === Context, so we can lazy-load the context itself
125 + const LazyProvider = lazy(() => fakeImport(Context));
126 +
127 + const root = ReactTestRenderer.create(
128 + <Suspense fallback={<Text text="Loading..." />}>
129 + <LazyProvider value="Hi">
130 + <ConsumerText />
131 + </LazyProvider>
132 + </Suspense>,
133 + {
134 + unstable_isConcurrent: true,
135 + },
136 + );
137 +
138 + await waitForAll(['Loading...']);
139 + expect(root).not.toMatchRenderedOutput('Hi');
140 +
141 + await act(() => resolveFakeImport(Context));
142 + assertLog(['Hi']);
143 + expect(root).toMatchRenderedOutput('Hi');
144 +
145 + // Should not suspend on update
146 + root.update(
147 + <Suspense fallback={<Text text="Loading..." />}>
148 + <LazyProvider value="Hi again">
149 + <ConsumerText />
150 + </LazyProvider>
151 + </Suspense>,
152 + );
153 + await waitForAll(['Hi again']);
154 + expect(root).toMatchRenderedOutput('Hi again');
155 + });
156 +
157 it('can resolve synchronously without suspending', async () => {
158 const LazyText = lazy(() => ({
159 then(cb) {
@@ -858,13 +896,20 @@ describe('ReactLazy', () => {
896 );
897 });
898
861 - it('throws with a useful error when wrapping Context with lazy()', async () => {
862 - const Context = React.createContext(null);
863 - const BadLazy = lazy(() => fakeImport(Context));
899 + it('renders a lazy context provider without value prop', async () => {
900 + // Context providers work when wrapped in lazy()
901 + const Context = React.createContext('default');
902 + const LazyProvider = lazy(() => fakeImport(Context));
903 +
904 + function ConsumerText() {
905 + return <Text text={React.useContext(Context)} />;
906 + }
907
908 const root = ReactTestRenderer.create(
909 <Suspense fallback={<Text text="Loading..." />}>
867 - <BadLazy />
910 + <LazyProvider value="provided">
911 + <ConsumerText />
912 + </LazyProvider>
913 </Suspense>,
914 {
915 unstable_isConcurrent: true,
@@ -873,16 +918,9 @@ describe('ReactLazy', () => {
918
919 await waitForAll(['Loading...']);
920
876 - await resolveFakeImport(Context);
877 - root.update(
878 - <Suspense fallback={<Text text="Loading..." />}>
879 - <BadLazy />
880 - </Suspense>,
881 - );
882 - await waitForThrow(
883 - 'Element type is invalid. Received a promise that resolves to: Context. ' +
884 - 'Lazy element type must resolve to a class or function.',
885 - );
921 + await act(() => resolveFakeImport(Context));
922 + assertLog(['provided']);
923 + expect(root).toMatchRenderedOutput('provided');
924 });
925
926 it('throws with a useful error when wrapping Context.Consumer with lazy()', async () => {
packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
+4 -5
@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps<mixed> = {
182 // $FlowFixMe[prop-missing]
183 return Object.prototype[Symbol.toStringTag];
184 case 'Provider':
185 - throw new Error(
186 - `Cannot render a Client Context Provider on the Server. ` +
187 - `Instead, you can export a Client Component wrapper ` +
188 - `that itself renders a Client Context Provider.`,
189 - );
185 + // Context.Provider === Context in React, so return the same reference.
186 + // This allows server components to render <ClientContext.Provider>
187 + // which will be serialized and executed on the client.
188 + return receiver;
189 case 'then':
190 throw new Error(
191 `Cannot await or return from a thenable. ` +
packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js
+4 -5
@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps<mixed> = {
182 // $FlowFixMe[prop-missing]
183 return Object.prototype[Symbol.toStringTag];
184 case 'Provider':
185 - throw new Error(
186 - `Cannot render a Client Context Provider on the Server. ` +
187 - `Instead, you can export a Client Component wrapper ` +
188 - `that itself renders a Client Context Provider.`,
189 - );
185 + // Context.Provider === Context in React, so return the same reference.
186 + // This allows server components to render <ClientContext.Provider>
187 + // which will be serialized and executed on the client.
188 + return receiver;
189 case 'then':
190 throw new Error(
191 `Cannot await or return from a thenable. ` +
packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
+4 -5
@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps<mixed> = {
182 // $FlowFixMe[prop-missing]
183 return Object.prototype[Symbol.toStringTag];
184 case 'Provider':
185 - throw new Error(
186 - `Cannot render a Client Context Provider on the Server. ` +
187 - `Instead, you can export a Client Component wrapper ` +
188 - `that itself renders a Client Context Provider.`,
189 - );
185 + // Context.Provider === Context in React, so return the same reference.
186 + // This allows server components to render <ClientContext.Provider>
187 + // which will be serialized and executed on the client.
188 + return receiver;
189 case 'then':
190 throw new Error(
191 `Cannot await or return from a thenable. ` +
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+54 -5
@@ -787,7 +787,7 @@ describe('ReactFlightDOM', () => {
787 <ClientModule.Component key="this adds instrumentation" />;
788 });
789
790 - it('throws when accessing a Context.Provider below the client exports', () => {
790 + it('does not throw when accessing a Context.Provider from client exports', () => {
791 const Context = React.createContext();
792 const ClientModule = clientExports({
793 Context,
@@ -795,11 +795,60 @@ describe('ReactFlightDOM', () => {
795 function dotting() {
796 return ClientModule.Context.Provider;
797 }
798 - expect(dotting).toThrowError(
799 - `Cannot render a Client Context Provider on the Server. ` +
800 - `Instead, you can export a Client Component wrapper ` +
801 - `that itself renders a Client Context Provider.`,
798 + expect(dotting).not.toThrowError();
799 + });
800 +
801 + it('can render a client Context.Provider from a server component', async () => {
802 + // Create a context in a client module
803 + const TestContext = React.createContext('default');
804 + const ClientModule = clientExports({
805 + TestContext,
806 + });
807 +
808 + // Client component that reads context
809 + function ClientConsumer() {
810 + const value = React.useContext(TestContext);
811 + return <span>{value}</span>;
812 + }
813 + const {ClientConsumer: ClientConsumerRef} = clientExports({ClientConsumer});
814 +
815 + function Print({response}) {
816 + return use(response);
817 + }
818 +
819 + function App({response}) {
820 + return (
821 + <Suspense fallback={<h1>Loading...</h1>}>
822 + <Print response={response} />
823 + </Suspense>
824 + );
825 + }
826 +
827 + // Server component that provides context
828 + function ServerApp() {
829 + return (
830 + <ClientModule.TestContext.Provider value="from-server">
831 + <div>
832 + <ClientConsumerRef />
833 + </div>
834 + </ClientModule.TestContext.Provider>
835 + );
836 + }
837 +
838 + const {writable, readable} = getTestStream();
839 + const {pipe} = await serverAct(() =>
840 + ReactServerDOMServer.renderToPipeableStream(<ServerApp />, webpackMap),
841 );
842 + pipe(writable);
843 + const response = ReactServerDOMClient.createFromReadableStream(readable);
844 +
845 + const container = document.createElement('div');
846 + const root = ReactDOMClient.createRoot(container);
847 + await act(() => {
848 + root.render(<App response={response} />);
849 + });
850 +
851 + expect(container.innerHTML).toBe('<div><span>from-server</span></div>');
852 });
853
854 it('should progressively reveal server components', async () => {
packages/react-server/src/ReactFlightServerTemporaryReferences.js
+4 -5
@@ -65,11 +65,10 @@ const proxyHandlers: Proxy$traps<mixed> = {
65 // $FlowFixMe[prop-missing]
66 return Object.prototype[Symbol.toStringTag];
67 case 'Provider':
68 - throw new Error(
69 - `Cannot render a Client Context Provider on the Server. ` +
70 - `Instead, you can export a Client Component wrapper ` +
71 - `that itself renders a Client Context Provider.`,
72 - );
68 + // Context.Provider === Context in React, so return the same reference.
69 + // This allows server components to render <ClientContext.Provider>
70 + // which will be serialized and executed on the client.
71 + return receiver;
72 case 'then':
73 // Allow returning a temporary reference from an async function
74 // Unlike regular Client References, a Promise would never have been serialized as