fix: `React.use` inside `React.lazy`-ed component on SSR (#33941)
Hiroshi Ogawa committed
Jul 28, 2025 at 17:36 UTC
cc015840ef72d48de86778785894c7ca44f4f856
2 files changed
+81
-6
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+57
@@ -6340,6 +6340,63 @@ describe('ReactDOMFizzServer', () => {
6340
expect(getVisibleChildren(container)).toEqual('Hi');
6341
});
6342
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
+
6400
it('useActionState hydrates without a mismatch', async () => {
6401
// This is testing an implementation detail: useActionState emits comment
6402
// nodes into the SSR stream, so this checks that they are handled correctly
packages/react-server/src/ReactFizzServer.js
+24
-6
@@ -4153,7 +4153,10 @@ function renderNode(
4153
// $FlowFixMe[method-unbinding]
4154
if (typeof x.then === 'function') {
4155
const wakeable: Wakeable = (x: any);
4156
- const thenableState = getThenableStateAfterSuspending();
4156
+ const thenableState =
4157
+ thrownValue === SuspenseException
4158
+ ? getThenableStateAfterSuspending()
4159
+ : null;
4160
const newTask = spawnNewSuspendedReplayTask(
4161
request,
4162
// $FlowFixMe: Refined.
@@ -4186,7 +4189,10 @@ function renderNode(
4189
// performance but it can lead to stack overflows in extremely deep trees.
4190
// We do have the ability to create a trampoile if this happens which makes
4191
// this kind of zero-cost.
4189
- const thenableState = getThenableStateAfterSuspending();
4192
+ const thenableState =
4193
+ thrownValue === SuspenseException
4194
+ ? getThenableStateAfterSuspending()
4195
+ : null;
4196
const newTask = spawnNewSuspendedReplayTask(
4197
request,
4198
// $FlowFixMe: Refined.
@@ -4246,7 +4252,10 @@ function renderNode(
4252
// $FlowFixMe[method-unbinding]
4253
if (typeof x.then === 'function') {
4254
const wakeable: Wakeable = (x: any);
4249
- const thenableState = getThenableStateAfterSuspending();
4255
+ const thenableState =
4256
+ thrownValue === SuspenseException
4257
+ ? getThenableStateAfterSuspending()
4258
+ : null;
4259
const newTask = spawnNewSuspendedRenderTask(
4260
request,
4261
// $FlowFixMe: Refined.
@@ -4317,7 +4326,10 @@ function renderNode(
4326
// performance but it can lead to stack overflows in extremely deep trees.
4327
// We do have the ability to create a trampoile if this happens which makes
4328
// this kind of zero-cost.
4320
- const thenableState = getThenableStateAfterSuspending();
4329
+ const thenableState =
4330
+ thrownValue === SuspenseException
4331
+ ? getThenableStateAfterSuspending()
4332
+ : null;
4333
const newTask = spawnNewSuspendedRenderTask(
4334
request,
4335
// $FlowFixMe: Refined.
@@ -5233,7 +5245,10 @@ function retryRenderTask(
5245
if (typeof x.then === 'function') {
5246
// Something suspended again, let's pick it back up later.
5247
segment.status = PENDING;
5236
- task.thenableState = getThenableStateAfterSuspending();
5248
+ task.thenableState =
5249
+ thrownValue === SuspenseException
5250
+ ? getThenableStateAfterSuspending()
5251
+ : null;
5252
const ping = task.ping;
5253
// We've asserted that x is a thenable above
5254
(x: any).then(ping, ping);
@@ -5338,7 +5353,10 @@ function retryReplayTask(request: Request, task: ReplayTask): void {
5353
// Something suspended again, let's pick it back up later.
5354
const ping = task.ping;
5355
x.then(ping, ping);
5341
- task.thenableState = getThenableStateAfterSuspending();
5356
+ task.thenableState =
5357
+ thrownValue === SuspenseException
5358
+ ? getThenableStateAfterSuspending()
5359
+ : null;
5360
return;
5361
}
5362
}