diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index fa89cf69ad67..89769fc64de0 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -931,6 +931,7 @@ type InitializationHandler = { value: any, reason: any, deps: number, + debugDeps: number, errored: boolean, }; let initializingHandler: null | InitializationHandler = null; @@ -1412,7 +1413,7 @@ function createElement( } return createLazyChunkWrapper(erroredChunk, validated); } - if (handler.deps > 0) { + if (handler.deps > (__DEV__ ? handler.debugDeps : 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. const blockedChunk: BlockedChunk> = @@ -1427,6 +1428,9 @@ function createElement( } return lazyNode; } + if (__DEV__ && handler.debugDeps > 0) { + handler.value = element; + } } if (__DEV__) { initializeElement(response, element, null); @@ -1669,6 +1673,19 @@ function fulfillReference( if (handler.deps === 0) { const chunk = handler.chunk; if (chunk === null || chunk.status !== BLOCKED) { + if (__DEV__ && chunk === null && handler.debugDeps > 0) { + // All debug-only deps (owner/stack from debug channel) have resolved. + // Re-initialize the element so _debugStack gets normalized into an Error + // object now that the raw stack string is available. + const elementValue = handler.value; + if ( + typeof elementValue === 'object' && + elementValue !== null && + elementValue.$$typeof === REACT_ELEMENT_TYPE + ) { + initializeElement(response, elementValue, null); + } + } return; } const resolveListeners = chunk.value; @@ -1745,23 +1762,28 @@ function waitForReference( path: Array, isAwaitingDebugInfo: boolean, // DEV-only ): T { - if ( - __DEV__ && - (response._debugChannel === undefined || - !response._debugChannel.hasReadable) - ) { + let isDebugOnlyDep = false; + if (__DEV__) { if ( referencedChunk.status === PENDING && parentObject[0] === REACT_ELEMENT_TYPE && (key === '4' || key === '5') ) { - // If the parent object is an unparsed React element tuple, and this is a reference - // to the owner or debug stack. Then we expect the chunk to have been emitted earlier - // in the stream. It might be blocked on other things but chunk should no longer be pending. - // If it's still pending that suggests that it was referencing an object in the debug - // channel, but no debug channel was wired up so it's missing. In this case we can just - // drop the debug info instead of halting the whole stream. - return (null: any); + // The parent object is an unparsed React element tuple and this is a reference + // to the owner (pos 4) or debug stack (pos 5) — dev-only metadata. + if ( + response._debugChannel === undefined || + !response._debugChannel.hasReadable + ) { + // No debug channel is wired up so this data will never arrive. + // Drop the reference instead of halting the stream. + return (null: any); + } + // A debug channel with a readable is active: the data will arrive, but + // owner/stack must never block element construction because that would + // wrap the element in a lazy chunk ($$typeof: REACT_LAZY_TYPE) and break + // isValidElement(), cloneElement(), and hydration. + isDebugOnlyDep = true; } } @@ -1769,6 +1791,9 @@ function waitForReference( if (initializingHandler) { handler = initializingHandler; handler.deps++; + if (__DEV__ && isDebugOnlyDep) { + handler.debugDeps++; + } } else { handler = initializingHandler = { parent: null, @@ -1776,6 +1801,7 @@ function waitForReference( value: null, reason: null, deps: 1, + debugDeps: __DEV__ && isDebugOnlyDep ? 1 : 0, errored: false, }; } @@ -1865,6 +1891,7 @@ function loadServerReference, T>( value: null, reason: null, deps: 1, + debugDeps: 0, errored: false, }; } @@ -2108,6 +2135,7 @@ function getOutlinedModel( value: null, reason: null, deps: 1, + debugDeps: 0, errored: false, }; } @@ -2127,6 +2155,7 @@ function getOutlinedModel( value: null, reason: referencedChunk.reason, deps: 0, + debugDeps: 0, errored: true, }; } @@ -2205,6 +2234,7 @@ function getOutlinedModel( value: null, reason: null, deps: 1, + debugDeps: 0, errored: false, }; } @@ -2224,6 +2254,7 @@ function getOutlinedModel( value: null, reason: chunk.reason, deps: 0, + debugDeps: 0, errored: true, }; } @@ -2377,6 +2408,7 @@ function parseModelString( value: null, reason: null, deps: 0, + debugDeps: 0, errored: false, }; } diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 0ac1f84cd396..080a180a8199 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -1909,4 +1909,53 @@ describe('ReactFlightDOMNode', () => { globalThis.eval = previousEval; } }); + + it('isValidElement returns true for elements deserialized with a delayed debugChannel', async () => { + function ServerApp() { + return ReactServer.createElement('div', null, 'hello'); + } + + const {delayedStream, resolveDelayedStream} = createDelayedStream(); + + const rscStream = await serverAct(() => + ReactServerDOMServer.renderToPipeableStream( + ReactServer.createElement(ServerApp, null), + webpackMap, + { + debugChannel: new Stream.Writable({ + write(chunk, encoding, callback) { + delayedStream.write(chunk, encoding); + callback(); + }, + final() { + delayedStream.end(); + }, + }), + }, + ), + ); + + const readable = new Stream.PassThrough(streamOptions); + rscStream.pipe(readable); + + const serverConsumerManifest = { + moduleMap: {}, + moduleLoading: webpackModuleLoading, + }; + + const response = ReactServerDOMClient.createFromNodeStream( + readable, + serverConsumerManifest, + {debugChannel: delayedStream}, + ); + + setTimeout(resolveDelayedStream); + + let deserialized; + await serverAct(async () => { + deserialized = await response; + }); + + expect(React.isValidElement(deserialized)).toBe(true); + }); });