@samitouri / QOS-React-1 / commits / cc015840ef

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 }