diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 3d125cc3104..cdf97e342f7 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -163,6 +163,7 @@ type BlockedChunk = { status: 'blocked', value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, + intermediaryValue?: mixed, _response: Response, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only @@ -568,6 +569,7 @@ type InitializationHandler = { }; let initializingHandler: null | InitializationHandler = null; let initializingChunk: null | BlockedChunk = null; +let isParsingRootModel = false; function initializeModelChunk(chunk: ResolvedModelChunk): void { const prevHandler = initializingHandler; @@ -584,9 +586,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { cyclicChunk.value = null; cyclicChunk.reason = null; - if (enableProfilerTimer && enableComponentPerformanceTrack) { - initializingChunk = cyclicChunk; - } + initializingChunk = cyclicChunk; try { const value: T = parseModel(chunk._response, resolvedModel); @@ -620,9 +620,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { erroredChunk.reason = error; } finally { initializingHandler = prevHandler; - if (enableProfilerTimer && enableComponentPerformanceTrack) { - initializingChunk = prevChunk; - } + initializingChunk = prevChunk; } } @@ -876,8 +874,18 @@ function createElement( return createLazyChunkWrapper(erroredChunk); } if (handler.deps > 0) { - // We have blocked references inside this Element but we can turn this into - // a Lazy node referencing this Element to let everything around it proceed. + // We have blocked references inside this element but we can turn this + // into a Lazy node referencing this element to let everything around it + // proceed. If the element is for the root model, we store it as an + // intermediary value on the initializing chunk, so that referencing + // models can peek into it to resolve their value. + if ( + isParsingRootModel && + initializingChunk && + !initializingChunk.intermediaryValue + ) { + initializingChunk.intermediaryValue = element; + } const blockedChunk: BlockedChunk> = createBlockedChunk(response); handler.value = element; @@ -967,6 +975,15 @@ function waitForReference( } else if (chunk.status === INITIALIZED) { value = chunk.value; continue; + } else if ( + // For the first iteration of the path we need the root model of the + // referenced chunk. If we have an intermediary value for it, we can + // peek into it, even if it's not fully resolved yet. + i === 1 && + referencedChunk.status === BLOCKED && + referencedChunk.intermediaryValue + ) { + value = referencedChunk.intermediaryValue; } else { // If we're not yet initialized we need to skip what we've already drilled // through and then wait for the next value to become available. @@ -3393,6 +3410,7 @@ function parseModel(response: Response, json: UninitializedModel): T { function createFromJSONCallback(response: Response) { // $FlowFixMe[missing-this-annot] return function (key: string, value: JSONValue) { + isParsingRootModel = key === ''; if (typeof value === 'string') { // We can't use .bind here because we need the "this" value. return parseModelString(response, this, key, value); 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 040bb046b55..c0533052450 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -533,6 +533,43 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('{"foo":1}{"foo":1}'); }); + it('should resolve deduped references in maps used in client component props', async () => { + const ClientComponent = clientExports(function ClientComponent({ + shared, + map, + }) { + expect(map.get(42)).toBe(shared); + return JSON.stringify({shared, map: Array.from(map)}); + }); + + function Server() { + const shared = {id: 42}; + const map = new Map([[42, shared]]); + + return ; + } + + 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( + '{"shared":{"id":42},"map":[[42,{"id":42}]]}', + ); + }); + it('should handle deduped props of re-used elements in fragments (same-chunk reference)', async () => { let resolveFooClientComponentChunk;