From 1b7910c64be09c0adc427a763f0c8202a7162ff4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 18 Jul 2025 15:52:16 +0900 Subject: [PATCH 1/3] fix: fix `React.use` in lazy component --- .../src/__tests__/ReactDOMFizzServer-test.js | 78 +++++++++++++++++++ packages/react-server/src/ReactFizzServer.js | 12 ++- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 0838371bef92c..1eedc97c51df4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -6340,6 +6340,84 @@ describe('ReactDOMFizzServer', () => { expect(getVisibleChildren(container)).toEqual('Hi'); }); + it('should correctly handle different promises in React.use() across lazy components', async () => { + const promise1 = new Promise(r => setTimeout(() => r('value1'), 50)); + const promise2 = new Promise(r => setTimeout(() => r('value2'), 300)); + // const promise1 = Promise.resolve('value1'); + // const promise2 = Promise.resolve('value2'); + + let component1Rendered = false; + let component2Rendered = false; + + function Component1() { + const data = React.use(promise1); + component1Rendered = true; + return ( +
+ {data} + + {/* + */} +
+ ); + } + + function Component2() { + const data = React.use(promise2); + component2Rendered = true; + return
{data}
; + } + + let promiseLazy; + const Component2Lazy = React.lazy(async () => { + promiseLazy ??= new Promise(r => setTimeout(r, 100)); + await promiseLazy; + return {default: Component2}; + }); + + function App() { + return ( +
+ + {/* + */} +
+ ); + } + + await act(async () => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); + + // Wait for the stream to complete + await act(async () => { + await promise1; + }); + await act(async () => { + await promiseLazy; + }); + await act(async () => { + await promise2; + }); + // await act(() => {}); + + expect(component1Rendered).toBe(true); + expect(component2Rendered).toBe(true); + + // Verify both components received the correct values + expect(getVisibleChildren(container)).toEqual( +
+
+ value1 +
+ value2 +
+
+
, + ); + }); + it('useActionState hydrates without a mismatch', async () => { // This is testing an implementation detail: useActionState emits comment // nodes into the SSR stream, so this checks that they are handled correctly diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 75267e0e4f00b..94edb2ba13770 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -2784,6 +2784,7 @@ function renderLazyComponent( // eslint-disable-next-line no-throw-literal throw null; } + // task.thenableState = null; renderElement(request, task, keyPath, Component, props, ref); } @@ -4153,7 +4154,7 @@ function renderNode( // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); - const thenableState = getThenableStateAfterSuspending(); + const thenableState = thrownValue === SuspenseException ? getThenableStateAfterSuspending() : null; const newTask = spawnNewSuspendedReplayTask( request, // $FlowFixMe: Refined. @@ -4246,7 +4247,8 @@ function renderNode( // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); - const thenableState = getThenableStateAfterSuspending(); + // const thenableState = getThenableStateAfterSuspending(); + const thenableState = thrownValue === SuspenseException ? getThenableStateAfterSuspending() : null; const newTask = spawnNewSuspendedRenderTask( request, // $FlowFixMe: Refined. @@ -5233,7 +5235,8 @@ function retryRenderTask( if (typeof x.then === 'function') { // Something suspended again, let's pick it back up later. segment.status = PENDING; - task.thenableState = getThenableStateAfterSuspending(); + // task.thenableState = getThenableStateAfterSuspending(); + task.thenableState = thrownValue === SuspenseException ? getThenableStateAfterSuspending() : null; const ping = task.ping; // We've asserted that x is a thenable above (x: any).then(ping, ping); @@ -5338,7 +5341,8 @@ function retryReplayTask(request: Request, task: ReplayTask): void { // Something suspended again, let's pick it back up later. const ping = task.ping; x.then(ping, ping); - task.thenableState = getThenableStateAfterSuspending(); + // task.thenableState = getThenableStateAfterSuspending(); + task.thenableState = thrownValue === SuspenseException ? getThenableStateAfterSuspending() : null; return; } } From 5c440af2b30b782bcb2c3ab79fb53a43403466c4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 18 Jul 2025 15:56:44 +0900 Subject: [PATCH 2/3] reset in renderLazyComponent --- .../src/__tests__/ReactDOMFizzServer-test.js | 41 +++++-------------- packages/react-server/src/ReactFizzServer.js | 15 ++++--- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 1eedc97c51df4..d6d85b96ce702 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -6341,48 +6341,35 @@ describe('ReactDOMFizzServer', () => { }); it('should correctly handle different promises in React.use() across lazy components', async () => { - const promise1 = new Promise(r => setTimeout(() => r('value1'), 50)); - const promise2 = new Promise(r => setTimeout(() => r('value2'), 300)); - // const promise1 = Promise.resolve('value1'); - // const promise2 = Promise.resolve('value2'); - - let component1Rendered = false; - let component2Rendered = false; + let promise1; + let promise2; + let promiseLazy; function Component1() { + promise1 ??= new Promise(r => setTimeout(() => r('value1'), 50)); const data = React.use(promise1); - component1Rendered = true; return (
{data} - {/* - */}
); } function Component2() { + promise2 ??= new Promise(r => setTimeout(() => r('value2'), 50)); const data = React.use(promise2); - component2Rendered = true; return
{data}
; } - let promiseLazy; const Component2Lazy = React.lazy(async () => { - promiseLazy ??= new Promise(r => setTimeout(r, 100)); + promiseLazy ??= new Promise(r => setTimeout(r, 50)); await promiseLazy; return {default: Component2}; }); function App() { - return ( -
- - {/* - */} -
- ); + return ; } await act(async () => { @@ -6390,7 +6377,7 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); - // Wait for the stream to complete + // Wait for promise to resolve await act(async () => { await promise1; }); @@ -6400,20 +6387,12 @@ describe('ReactDOMFizzServer', () => { await act(async () => { await promise2; }); - // await act(() => {}); - - expect(component1Rendered).toBe(true); - expect(component2Rendered).toBe(true); // Verify both components received the correct values expect(getVisibleChildren(container)).toEqual(
-
- value1 -
- value2 -
-
+ value1 +
value2
, ); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 94edb2ba13770..de75e50ebed1e 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -2772,6 +2772,9 @@ function renderLazyComponent( props: Object, ref: any, ): void { + const prevThenableState = task.thenableState; + task.thenableState = null; + prepareToUseThenableState(prevThenableState); let Component; if (__DEV__) { Component = callLazyInitInDEV(lazyComponent); @@ -2784,7 +2787,6 @@ function renderLazyComponent( // eslint-disable-next-line no-throw-literal throw null; } - // task.thenableState = null; renderElement(request, task, keyPath, Component, props, ref); } @@ -4154,7 +4156,7 @@ function renderNode( // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); - const thenableState = thrownValue === SuspenseException ? getThenableStateAfterSuspending() : null; + const thenableState = getThenableStateAfterSuspending(); const newTask = spawnNewSuspendedReplayTask( request, // $FlowFixMe: Refined. @@ -4247,8 +4249,7 @@ function renderNode( // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); - // const thenableState = getThenableStateAfterSuspending(); - const thenableState = thrownValue === SuspenseException ? getThenableStateAfterSuspending() : null; + const thenableState = getThenableStateAfterSuspending(); const newTask = spawnNewSuspendedRenderTask( request, // $FlowFixMe: Refined. @@ -5235,8 +5236,7 @@ function retryRenderTask( if (typeof x.then === 'function') { // Something suspended again, let's pick it back up later. segment.status = PENDING; - // task.thenableState = getThenableStateAfterSuspending(); - task.thenableState = thrownValue === SuspenseException ? getThenableStateAfterSuspending() : null; + task.thenableState = getThenableStateAfterSuspending(); const ping = task.ping; // We've asserted that x is a thenable above (x: any).then(ping, ping); @@ -5341,8 +5341,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { // Something suspended again, let's pick it back up later. const ping = task.ping; x.then(ping, ping); - // task.thenableState = getThenableStateAfterSuspending(); - task.thenableState = thrownValue === SuspenseException ? getThenableStateAfterSuspending() : null; + task.thenableState = getThenableStateAfterSuspending(); return; } } From 3eb39aea06d9367050a6cb41d2fe6c2447c082ac Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 18 Jul 2025 16:48:18 +0900 Subject: [PATCH 3/3] fix: back to getThenableStateAfterSuspending tweak --- packages/react-server/src/ReactFizzServer.js | 33 ++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index de75e50ebed1e..b0df51e0bba64 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -2772,9 +2772,6 @@ function renderLazyComponent( props: Object, ref: any, ): void { - const prevThenableState = task.thenableState; - task.thenableState = null; - prepareToUseThenableState(prevThenableState); let Component; if (__DEV__) { Component = callLazyInitInDEV(lazyComponent); @@ -4156,7 +4153,10 @@ function renderNode( // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); - const thenableState = getThenableStateAfterSuspending(); + const thenableState = + thrownValue === SuspenseException + ? getThenableStateAfterSuspending() + : null; const newTask = spawnNewSuspendedReplayTask( request, // $FlowFixMe: Refined. @@ -4189,7 +4189,10 @@ function renderNode( // performance but it can lead to stack overflows in extremely deep trees. // We do have the ability to create a trampoile if this happens which makes // this kind of zero-cost. - const thenableState = getThenableStateAfterSuspending(); + const thenableState = + thrownValue === SuspenseException + ? getThenableStateAfterSuspending() + : null; const newTask = spawnNewSuspendedReplayTask( request, // $FlowFixMe: Refined. @@ -4249,7 +4252,10 @@ function renderNode( // $FlowFixMe[method-unbinding] if (typeof x.then === 'function') { const wakeable: Wakeable = (x: any); - const thenableState = getThenableStateAfterSuspending(); + const thenableState = + thrownValue === SuspenseException + ? getThenableStateAfterSuspending() + : null; const newTask = spawnNewSuspendedRenderTask( request, // $FlowFixMe: Refined. @@ -4320,7 +4326,10 @@ function renderNode( // performance but it can lead to stack overflows in extremely deep trees. // We do have the ability to create a trampoile if this happens which makes // this kind of zero-cost. - const thenableState = getThenableStateAfterSuspending(); + const thenableState = + thrownValue === SuspenseException + ? getThenableStateAfterSuspending() + : null; const newTask = spawnNewSuspendedRenderTask( request, // $FlowFixMe: Refined. @@ -5236,7 +5245,10 @@ function retryRenderTask( if (typeof x.then === 'function') { // Something suspended again, let's pick it back up later. segment.status = PENDING; - task.thenableState = getThenableStateAfterSuspending(); + task.thenableState = + thrownValue === SuspenseException + ? getThenableStateAfterSuspending() + : null; const ping = task.ping; // We've asserted that x is a thenable above (x: any).then(ping, ping); @@ -5341,7 +5353,10 @@ function retryReplayTask(request: Request, task: ReplayTask): void { // Something suspended again, let's pick it back up later. const ping = task.ping; x.then(ping, ping); - task.thenableState = getThenableStateAfterSuspending(); + task.thenableState = + thrownValue === SuspenseException + ? getThenableStateAfterSuspending() + : null; return; } }