diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 0838371bef92c..d6d85b96ce702 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -6340,6 +6340,63 @@ describe('ReactDOMFizzServer', () => { expect(getVisibleChildren(container)).toEqual('Hi'); }); + it('should correctly handle different promises in React.use() across lazy components', async () => { + let promise1; + let promise2; + let promiseLazy; + + function Component1() { + promise1 ??= new Promise(r => setTimeout(() => r('value1'), 50)); + const data = React.use(promise1); + return ( +
+ {data} + +
+ ); + } + + function Component2() { + promise2 ??= new Promise(r => setTimeout(() => r('value2'), 50)); + const data = React.use(promise2); + return
{data}
; + } + + const Component2Lazy = React.lazy(async () => { + promiseLazy ??= new Promise(r => setTimeout(r, 50)); + await promiseLazy; + return {default: Component2}; + }); + + function App() { + return ; + } + + await act(async () => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); + + // Wait for promise to resolve + await act(async () => { + await promise1; + }); + await act(async () => { + await promiseLazy; + }); + await act(async () => { + await promise2; + }); + + // 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..b0df51e0bba64 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -4153,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. @@ -4186,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. @@ -4246,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. @@ -4317,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. @@ -5233,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); @@ -5338,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; } }