diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 597536c961333..83c509dab46a8 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -937,26 +937,35 @@ function waitForReference( } value = value[path[i]]; } - parentObject[key] = map(response, value); + const mappedValue = map(response, value); + parentObject[key] = mappedValue; // If this is the root object for a model reference, where `handler.value` // is a stale `null`, the resolved value can be used directly. if (key === '' && handler.value === null) { - handler.value = parentObject[key]; + handler.value = mappedValue; } - // If the parent object is an unparsed React element tuple and its outlined - // props have now been resolved, we also need to update the props of the - // parsed element object (i.e. handler.value). + // If the parent object is an unparsed React element tuple, we also need to + // update the props and owner of the parsed element object (i.e. + // handler.value). if ( parentObject[0] === REACT_ELEMENT_TYPE && - key === '3' && typeof handler.value === 'object' && handler.value !== null && - handler.value.$$typeof === REACT_ELEMENT_TYPE && - handler.value.props === null + handler.value.$$typeof === REACT_ELEMENT_TYPE ) { - handler.value.props = parentObject[key]; + const element: any = handler.value; + switch (key) { + case '3': + element.props = mappedValue; + break; + case '4': + if (__DEV__) { + element._owner = mappedValue; + } + break; + } } handler.deps--; 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 fa1e65862564e..0408a63fc512f 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -630,6 +630,119 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('
'); }); + it('should handle references to deduped owner objects', async () => { + // This is replicating React components as generated by @svgr/webpack: + let path1a: React.ReactNode; + let path1b: React.ReactNode; + let path2: React.ReactNode; + + function Svg1() { + return ReactServer.createElement( + 'svg', + {id: '1'}, + path1a || (path1a = ReactServer.createElement('path', {})), + path1b || (path1b = ReactServer.createElement('path', {})), + ); + } + + function Svg2() { + return ReactServer.createElement( + 'svg', + {id: '2'}, + path2 || (path2 = ReactServer.createElement('path', {})), + ); + } + + function Server() { + return ReactServer.createElement( + ReactServer.Fragment, + {}, + ReactServer.createElement(Svg1), + ReactServer.createElement(Svg2), + ); + } + + let stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(, webpackMap), + ); + + function ClientRoot({response}) { + return use(response); + } + + let response = ReactServerDOMClient.createFromReadableStream(stream); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + + const expectedHtml = + ''; + + expect(container.innerHTML).toBe(expectedHtml); + + // Render a second time: + + // Assigning the path elements to variables in module scope (here simulated + // with the test's function scope), and rendering a second time, prevents + // the owner of the path elements (i.e. Svg1/Svg2) to be deduped. The owner + // of the path in Svg1 is fully inlined. The owner of the owner of the path + // in Svg2 is Server, which is deduped and replaced with a reference to the + // owner of the owner of the path in Svg1. This nested owner is actually + // Server from the previous render pass, which is kinda broken and libraries + // probably shouldn't generate code like this. This reference can only be + // resolved properly if owners are specifically handled when resolving + // outlined models. + + stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(, webpackMap), + ); + + response = ReactServerDOMClient.createFromReadableStream(stream); + + await act(() => { + root.render(); + }); + + expect(container.innerHTML).toBe(expectedHtml); + + if (__DEV__) { + const resolvedPath1b = await response.value[0].props.children[1]._payload; + + expect(resolvedPath1b._owner).toEqual( + expect.objectContaining({ + name: 'Svg1', + env: 'Server', + key: null, + owner: expect.objectContaining({ + name: 'Server', + env: 'Server', + key: null, + owner: null, + }), + }), + ); + + const resolvedPath2 = response.value[1].props.children; + + expect(resolvedPath2._owner).toEqual( + expect.objectContaining({ + name: 'Svg2', + env: 'Server', + key: null, + owner: expect.objectContaining({ + name: 'Server', + env: 'Server', + key: null, + owner: null, + }), + }), + ); + } + }); + it('should progressively reveal server components', async () => { let reportedErrors = []; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index f0e632c5499b0..5d79482de0186 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2665,6 +2665,9 @@ function renderModelDestructive( case '3': propertyName = 'props'; break; + case '4': + propertyName = '_owner'; + break; } } writtenObjects.set(value, parentReference + ':' + propertyName);