[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>
This commit is contained in:
Ricky
2026-02-03 10:22:57 -05:00
committed by GitHub
parent 3419420e8b
commit 3e00319b35
9 changed files with 145 additions and 40 deletions

View File

@@ -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}) {
<Foo>{dedupedChild}</Foo>
<Bar>{Promise.resolve([dedupedChild])}</Bar>
<Navigate />
<ClientContext value="from server">
<div>
<ClientReadContext />
</div>
</ClientContext>
{prerender ? null : ( // TODO: prerender is broken for large content for some reason.
<React.Suspense fallback={null}>
<LargeContent />

View File

@@ -0,0 +1,12 @@
'use client';
import {createContext, use} from 'react';
const ClientContext = createContext(null);
function ClientReadContext() {
const value = use(ClientContext);
return <p>{value}</p>;
}
export {ClientContext, ClientReadContext};

View File

@@ -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);
}
}

View File

@@ -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 <Text text={React.useContext(Context)} />;
}
// Context.Provider === Context, so we can lazy-load the context itself
const LazyProvider = lazy(() => fakeImport(Context));
const root = ReactTestRenderer.create(
<Suspense fallback={<Text text="Loading..." />}>
<LazyProvider value="Hi">
<ConsumerText />
</LazyProvider>
</Suspense>,
{
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(
<Suspense fallback={<Text text="Loading..." />}>
<LazyProvider value="Hi again">
<ConsumerText />
</LazyProvider>
</Suspense>,
);
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 <Text text={React.useContext(Context)} />;
}
const root = ReactTestRenderer.create(
<Suspense fallback={<Text text="Loading..." />}>
<BadLazy />
<LazyProvider value="provided">
<ConsumerText />
</LazyProvider>
</Suspense>,
{
unstable_isConcurrent: true,
@@ -873,16 +918,9 @@ describe('ReactLazy', () => {
await waitForAll(['Loading...']);
await resolveFakeImport(Context);
root.update(
<Suspense fallback={<Text text="Loading..." />}>
<BadLazy />
</Suspense>,
);
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 () => {

View File

@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps<mixed> = {
// $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 <ClientContext.Provider>
// which will be serialized and executed on the client.
return receiver;
case 'then':
throw new Error(
`Cannot await or return from a thenable. ` +

View File

@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps<mixed> = {
// $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 <ClientContext.Provider>
// which will be serialized and executed on the client.
return receiver;
case 'then':
throw new Error(
`Cannot await or return from a thenable. ` +

View File

@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps<mixed> = {
// $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 <ClientContext.Provider>
// which will be serialized and executed on the client.
return receiver;
case 'then':
throw new Error(
`Cannot await or return from a thenable. ` +

View File

@@ -787,7 +787,7 @@ describe('ReactFlightDOM', () => {
<ClientModule.Component key="this adds instrumentation" />;
});
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 <span>{value}</span>;
}
const {ClientConsumer: ClientConsumerRef} = clientExports({ClientConsumer});
function Print({response}) {
return use(response);
}
function App({response}) {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<Print response={response} />
</Suspense>
);
}
// Server component that provides context
function ServerApp() {
return (
<ClientModule.TestContext.Provider value="from-server">
<div>
<ClientConsumerRef />
</div>
</ClientModule.TestContext.Provider>
);
}
const {writable, readable} = getTestStream();
const {pipe} = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(<ServerApp />, webpackMap),
);
pipe(writable);
const response = ReactServerDOMClient.createFromReadableStream(readable);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App response={response} />);
});
expect(container.innerHTML).toBe('<div><span>from-server</span></div>');
});
it('should progressively reveal server components', async () => {

View File

@@ -65,11 +65,10 @@ const proxyHandlers: Proxy$traps<mixed> = {
// $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 <ClientContext.Provider>
// 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