Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion fixtures/flight/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 />
Expand Down
12 changes: 12 additions & 0 deletions fixtures/flight/src/ClientContext.js
Original file line number Diff line number Diff line change
@@ -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};
5 changes: 5 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}

Expand Down
66 changes: 52 additions & 14 deletions packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. ` +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. ` +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. ` +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -787,19 +787,68 @@ 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,
});
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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading