From 76002254b7e3270da199eec1e6f9a0577b988a36 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 25 Jul 2024 01:34:41 +0200 Subject: [PATCH] Fix resolving of references to deduped props in lazy elements (#30441) When a model references a deduped object of a blocked element that has subsequently been turned into a lazy element, we need to wait for the lazy element's chunk to resolve before resolving the reference. Without the fix, the new test failed with the following runtime error: ``` TypeError: Cannot read properties of undefined (reading 'children') 1003 | let value = chunk.value; 1004 | for (let i = 1; i < path.length; i++) { > 1005 | value = value[path[i]]; | ^ 1006 | } 1007 | const chunkValue = map(response, value); 1008 | if (__DEV__ && chunk._debugInfo) { at getOutlinedModel (packages/react-client/src/ReactFlightClient.js:1005:26) ``` The bug was uncovered after updating React in Next.js in https://github.com/vercel/next.js/pull/66711. --- .../react-client/src/ReactFlightClient.js | 17 ++++- .../__tests__/ReactFlightDOMBrowser-test.js | 69 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index b4b8c406424b1..fbebe2bdcdfd2 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -827,7 +827,7 @@ function getChunk(response: Response, id: number): SomeChunk { } function waitForReference( - referencedChunk: PendingChunk | BlockedChunk, + referencedChunk: SomeChunk, parentObject: Object, key: string, response: Response, @@ -1003,6 +1003,21 @@ function getOutlinedModel( let value = chunk.value; for (let i = 1; i < path.length; i++) { value = value[path[i]]; + if (value.$$typeof === REACT_LAZY_TYPE) { + const referencedChunk: SomeChunk = value._payload; + if (referencedChunk.status === INITIALIZED) { + value = referencedChunk.value; + } else { + return waitForReference( + referencedChunk, + parentObject, + key, + response, + map, + path.slice(i), + ); + } + } } const chunkValue = map(response, value); if (__DEV__ && chunk._debugInfo) { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 49f2823c8b387..def347e83a18b 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -456,6 +456,75 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('{}'); }); + it('should resolve deduped objects in blocked models referencing other blocked models with blocked references', async () => { + let resolveFooClientComponentChunk; + let resolveBarClientComponentChunk; + + function PassthroughServerComponent({children}) { + return children; + } + + const FooClient = clientExports( + function FooClient({children}) { + return JSON.stringify(children); + }, + '1', + '/foo.js', + new Promise(resolve => (resolveFooClientComponentChunk = resolve)), + ); + + const BarClient = clientExports( + function BarClient() { + return 'not used'; + }, + '2', + '/bar.js', + new Promise(resolve => (resolveBarClientComponentChunk = resolve)), + ); + + const shared = {foo: 1}; + + function Server() { + return ( + <> + + + {shared} + + + + {shared} + + + ); + } + + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(, webpackMap), + ); + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream(stream); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + + expect(container.innerHTML).toBe(''); + + await act(() => { + resolveFooClientComponentChunk(); + resolveBarClientComponentChunk(); + }); + + expect(container.innerHTML).toBe('{"foo":1}{"foo":1}'); + }); + it('should progressively reveal server components', async () => { let reportedErrors = [];