[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