mirror of
https://github.com/facebook/react.git
synced 2026-02-21 19:31:52 +00:00
[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:
@@ -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 />
|
||||
|
||||
12
fixtures/flight/src/ClientContext.js
Normal file
12
fixtures/flight/src/ClientContext.js
Normal 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};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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. ` +
|
||||
|
||||
@@ -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. ` +
|
||||
|
||||
@@ -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. ` +
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user