Skip to content

Commit cc01584

Browse files
authored
fix: React.use inside React.lazy-ed component on SSR (facebook#33941)
1 parent 19baee8 commit cc01584

File tree

2 files changed

+81
-6
lines changed

2 files changed

+81
-6
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6340,6 +6340,63 @@ describe('ReactDOMFizzServer', () => {
63406340
expect(getVisibleChildren(container)).toEqual('Hi');
63416341
});
63426342

6343+
it('should correctly handle different promises in React.use() across lazy components', async () => {
6344+
let promise1;
6345+
let promise2;
6346+
let promiseLazy;
6347+
6348+
function Component1() {
6349+
promise1 ??= new Promise(r => setTimeout(() => r('value1'), 50));
6350+
const data = React.use(promise1);
6351+
return (
6352+
<div>
6353+
{data}
6354+
<Component2Lazy />
6355+
</div>
6356+
);
6357+
}
6358+
6359+
function Component2() {
6360+
promise2 ??= new Promise(r => setTimeout(() => r('value2'), 50));
6361+
const data = React.use(promise2);
6362+
return <div>{data}</div>;
6363+
}
6364+
6365+
const Component2Lazy = React.lazy(async () => {
6366+
promiseLazy ??= new Promise(r => setTimeout(r, 50));
6367+
await promiseLazy;
6368+
return {default: Component2};
6369+
});
6370+
6371+
function App() {
6372+
return <Component1 />;
6373+
}
6374+
6375+
await act(async () => {
6376+
const {pipe} = renderToPipeableStream(<App />);
6377+
pipe(writable);
6378+
});
6379+
6380+
// Wait for promise to resolve
6381+
await act(async () => {
6382+
await promise1;
6383+
});
6384+
await act(async () => {
6385+
await promiseLazy;
6386+
});
6387+
await act(async () => {
6388+
await promise2;
6389+
});
6390+
6391+
// Verify both components received the correct values
6392+
expect(getVisibleChildren(container)).toEqual(
6393+
<div>
6394+
value1
6395+
<div>value2</div>
6396+
</div>,
6397+
);
6398+
});
6399+
63436400
it('useActionState hydrates without a mismatch', async () => {
63446401
// This is testing an implementation detail: useActionState emits comment
63456402
// nodes into the SSR stream, so this checks that they are handled correctly

packages/react-server/src/ReactFizzServer.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4153,7 +4153,10 @@ function renderNode(
41534153
// $FlowFixMe[method-unbinding]
41544154
if (typeof x.then === 'function') {
41554155
const wakeable: Wakeable = (x: any);
4156-
const thenableState = getThenableStateAfterSuspending();
4156+
const thenableState =
4157+
thrownValue === SuspenseException
4158+
? getThenableStateAfterSuspending()
4159+
: null;
41574160
const newTask = spawnNewSuspendedReplayTask(
41584161
request,
41594162
// $FlowFixMe: Refined.
@@ -4186,7 +4189,10 @@ function renderNode(
41864189
// performance but it can lead to stack overflows in extremely deep trees.
41874190
// We do have the ability to create a trampoile if this happens which makes
41884191
// this kind of zero-cost.
4189-
const thenableState = getThenableStateAfterSuspending();
4192+
const thenableState =
4193+
thrownValue === SuspenseException
4194+
? getThenableStateAfterSuspending()
4195+
: null;
41904196
const newTask = spawnNewSuspendedReplayTask(
41914197
request,
41924198
// $FlowFixMe: Refined.
@@ -4246,7 +4252,10 @@ function renderNode(
42464252
// $FlowFixMe[method-unbinding]
42474253
if (typeof x.then === 'function') {
42484254
const wakeable: Wakeable = (x: any);
4249-
const thenableState = getThenableStateAfterSuspending();
4255+
const thenableState =
4256+
thrownValue === SuspenseException
4257+
? getThenableStateAfterSuspending()
4258+
: null;
42504259
const newTask = spawnNewSuspendedRenderTask(
42514260
request,
42524261
// $FlowFixMe: Refined.
@@ -4317,7 +4326,10 @@ function renderNode(
43174326
// performance but it can lead to stack overflows in extremely deep trees.
43184327
// We do have the ability to create a trampoile if this happens which makes
43194328
// this kind of zero-cost.
4320-
const thenableState = getThenableStateAfterSuspending();
4329+
const thenableState =
4330+
thrownValue === SuspenseException
4331+
? getThenableStateAfterSuspending()
4332+
: null;
43214333
const newTask = spawnNewSuspendedRenderTask(
43224334
request,
43234335
// $FlowFixMe: Refined.
@@ -5233,7 +5245,10 @@ function retryRenderTask(
52335245
if (typeof x.then === 'function') {
52345246
// Something suspended again, let's pick it back up later.
52355247
segment.status = PENDING;
5236-
task.thenableState = getThenableStateAfterSuspending();
5248+
task.thenableState =
5249+
thrownValue === SuspenseException
5250+
? getThenableStateAfterSuspending()
5251+
: null;
52375252
const ping = task.ping;
52385253
// We've asserted that x is a thenable above
52395254
(x: any).then(ping, ping);
@@ -5338,7 +5353,10 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
53385353
// Something suspended again, let's pick it back up later.
53395354
const ping = task.ping;
53405355
x.then(ping, ping);
5341-
task.thenableState = getThenableStateAfterSuspending();
5356+
task.thenableState =
5357+
thrownValue === SuspenseException
5358+
? getThenableStateAfterSuspending()
5359+
: null;
53425360
return;
53435361
}
53445362
}

0 commit comments

Comments
 (0)