From 5dad2b47b87d93db51f3bd41453a39124127b75d Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:44:04 +0000 Subject: [PATCH 1/6] [DevTools] Fix commit index reset when switching profiler roots (#35672) Fixes https://github.com/facebook/react/issues/31463, https://github.com/facebook/react/issues/30114. When switching between roots in the profiler flamegraph, the commit index was preserved from the previous root. This caused an error "Invalid commit X. There are only Y commits." when the new root had fewer commits than the selected index. This fix resets the commit index to 0 (or null if no commits) when the commitData changes, which happens when switching roots. --- .../src/__tests__/profilerContext-test.js | 78 +++++++++++++++++++ .../useCommitFilteringAndNavigation.js | 8 ++ 2 files changed, 86 insertions(+) diff --git a/packages/react-devtools-shared/src/__tests__/profilerContext-test.js b/packages/react-devtools-shared/src/__tests__/profilerContext-test.js index a1e47defa2a6..9688adebf8be 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerContext-test.js @@ -776,6 +776,84 @@ describe('ProfilerContext', () => { document.body.removeChild(profilerContainer); }); + it('should reset commit index when switching to a different root', async () => { + const Parent = () => ; + const Child = () => null; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + + const rootA = ReactDOMClient.createRoot(containerA); + const rootB = ReactDOMClient.createRoot(containerB); + + utils.act(() => rootA.render()); + utils.act(() => rootB.render()); + + // Profile and record different numbers of commits for each root + // Root A: 5 commits, Root B: 2 commits + await utils.actAsync(() => store.profilerStore.startProfiling()); + await utils.actAsync(() => rootA.render()); // Root A commit 1 + await utils.actAsync(() => rootA.render()); // Root A commit 2 + await utils.actAsync(() => rootA.render()); // Root A commit 3 + await utils.actAsync(() => rootA.render()); // Root A commit 4 + await utils.actAsync(() => rootA.render()); // Root A commit 5 + await utils.actAsync(() => rootB.render()); // Root B commit 1 + await utils.actAsync(() => rootB.render()); // Root B commit 2 + await utils.actAsync(() => store.profilerStore.stopProfiling()); + + let context: Context = ((null: any): Context); + function ContextReader() { + context = React.useContext(ProfilerContext); + return null; + } + + await utils.actAsync(() => + TestRenderer.create( + + + , + ), + ); + + // Verify we have profiling data for both roots + expect(context.didRecordCommits).toBe(true); + expect(context.profilingData).not.toBeNull(); + + const rootIDs = Array.from(context.profilingData.dataForRoots.keys()); + expect(rootIDs.length).toBe(2); + + const [rootAID, rootBID] = rootIDs; + const rootAData = context.profilingData.dataForRoots.get(rootAID); + const rootBData = context.profilingData.dataForRoots.get(rootBID); + + expect(rootAData.commitData.length).toBe(5); + expect(rootBData.commitData.length).toBe(2); + + // Select root A and navigate to commit 4 (index 3) + await utils.actAsync(() => context.setRootID(rootAID)); + expect(context.rootID).toBe(rootAID); + expect(context.selectedCommitIndex).toBe(0); + + await utils.actAsync(() => context.selectCommitIndex(3)); + expect(context.selectedCommitIndex).toBe(3); + + // Switch to root B which only has 2 commits + // The commit index should be reset to 0, not stay at 3 (which would be invalid) + await utils.actAsync(() => context.setRootID(rootBID)); + expect(context.rootID).toBe(rootBID); + // Should be reset to 0 since commit 3 doesn't exist in root B + expect(context.selectedCommitIndex).toBe(0); + + // Verify we can still navigate in root B + await utils.actAsync(() => context.selectCommitIndex(1)); + expect(context.selectedCommitIndex).toBe(1); + + // Switch back to root A - should reset to 0 + await utils.actAsync(() => context.setRootID(rootAID)); + expect(context.rootID).toBe(rootAID); + expect(context.selectedCommitIndex).toBe(0); + }); + it('should handle commit selection edge cases when filtering commits', async () => { const Scheduler = require('scheduler'); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/useCommitFilteringAndNavigation.js b/packages/react-devtools-shared/src/devtools/views/Profiler/useCommitFilteringAndNavigation.js index e309e6a3cf39..d3369b4f9538 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/useCommitFilteringAndNavigation.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/useCommitFilteringAndNavigation.js @@ -45,6 +45,14 @@ export function useCommitFilteringAndNavigation( null, ); + // Reset commit index when commitData changes (e.g., when switching roots). + const [previousCommitData, setPreviousCommitData] = + useState>(commitData); + if (previousCommitData !== commitData) { + setPreviousCommitData(commitData); + selectCommitIndex(commitData.length > 0 ? 0 : null); + } + const calculateFilteredIndices = useCallback( (enabled: boolean, minDuration: number): Array => { return commitData.reduce((reduced: Array, commitDatum, index) => { From b1533b034ee5e38e825fe20d789f2e83ede163ad Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Tue, 3 Feb 2026 15:29:51 +0100 Subject: [PATCH 2/6] [Flight] Allow overriding `request.timeOrigin` via `options.startTime` (#35598) Currently, IO that finished before the request started is not considered IO: https://github.com/facebook/react/blob/6a0ab4d2dd62dfdf8881ba1c3443c6d5acedc871/packages/react-server/src/ReactFlightServer.js#L5338-L5343 This leads to loss of debug info when a flight stream is deserialized and serialized again. We can solve this by allowing "when the the request started" to be set to a point in the past, when the original stream started by doing ```js const startTime = performance.now() + performance.timeOrigin // ... stuff happens and time passes... ReactServer.renderToReadableStream(..., { startTime }) ``` --- .../react-markup/src/ReactMarkupServer.js | 1 + .../src/ReactNoopFlightServer.js | 2 + .../src/server/ReactFlightDOMServerNode.js | 4 + .../src/server/ReactFlightDOMServerBrowser.js | 4 + .../src/server/ReactFlightDOMServerEdge.js | 4 + .../src/server/ReactFlightDOMServerNode.js | 6 + .../src/server/ReactFlightDOMServerBrowser.js | 3 + .../src/server/ReactFlightDOMServerEdge.js | 3 + .../src/server/ReactFlightDOMServerNode.js | 6 + .../src/server/ReactFlightDOMServerNode.js | 6 + .../src/__tests__/ReactFlightDOMNode-test.js | 292 +++++++++++++++++- .../src/server/ReactFlightDOMServerBrowser.js | 3 + .../src/server/ReactFlightDOMServerEdge.js | 3 + .../src/server/ReactFlightDOMServerNode.js | 6 + .../react-server/src/ReactFlightServer.js | 15 +- 15 files changed, 356 insertions(+), 2 deletions(-) diff --git a/packages/react-markup/src/ReactMarkupServer.js b/packages/react-markup/src/ReactMarkupServer.js index 5d22f1a4c94f..95a5ce51c3e1 100644 --- a/packages/react-markup/src/ReactMarkupServer.js +++ b/packages/react-markup/src/ReactMarkupServer.js @@ -184,6 +184,7 @@ export function experimental_renderToHTML( handleFlightError, options ? options.identifierPrefix : undefined, undefined, + undefined, 'Markup', undefined, false, diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 469e5465f308..752652259768 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -73,6 +73,7 @@ type Options = { signal?: AbortSignal, debugChannel?: {onMessage?: (message: string) => void}, onError?: (error: mixed) => void, + startTime?: number, }; function render(model: ReactClientValue, options?: Options): Destination { @@ -84,6 +85,7 @@ function render(model: ReactClientValue, options?: Options): Destination { options ? options.onError : undefined, options ? options.identifierPrefix : undefined, undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, __DEV__ && options && options.debugChannel !== undefined, diff --git a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js index 8e0799fb0209..9c167223bacc 100644 --- a/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js @@ -146,6 +146,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -183,6 +184,7 @@ function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannel !== undefined, @@ -272,6 +274,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -303,6 +306,7 @@ function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js index bdaadd66684a..aa677216d0a0 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js @@ -12,6 +12,7 @@ import type { ReactClientValue, } from 'react-server/src/ReactFlightServer'; import type {ReactFormState, Thenable} from 'shared/ReactTypes'; + import { preloadModule, requireModule, @@ -67,6 +68,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -128,6 +130,7 @@ export function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -215,6 +218,7 @@ export function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js index 83150996ae69..18813139526f 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js @@ -12,6 +12,7 @@ import type { ReactClientValue, } from 'react-server/src/ReactFlightServer'; import type {ReactFormState, Thenable} from 'shared/ReactTypes'; + import { preloadModule, requireModule, @@ -72,6 +73,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -133,6 +135,7 @@ export function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -220,6 +223,7 @@ export function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js index c5903c41ed49..910561aca51a 100644 --- a/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js @@ -159,6 +159,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -195,6 +196,7 @@ export function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannel !== undefined, @@ -352,6 +354,7 @@ export function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -434,6 +437,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -464,6 +468,7 @@ export function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, @@ -526,6 +531,7 @@ export function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js index 9388e5790f18..0100789347b0 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js @@ -64,6 +64,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -126,6 +127,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -214,6 +216,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js index f6a8fcc9abcd..18e71fdeab6c 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js @@ -69,6 +69,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -131,6 +132,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -219,6 +221,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js index 74d379f53a02..1590aa6a37c9 100644 --- a/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js @@ -152,6 +152,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -189,6 +190,7 @@ function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannel !== undefined, @@ -347,6 +349,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -429,6 +432,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -460,6 +464,7 @@ function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, @@ -523,6 +528,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js index 9a75c20395bc..f118d764447f 100644 --- a/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-unbundled/src/server/ReactFlightDOMServerNode.js @@ -152,6 +152,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -189,6 +190,7 @@ function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -347,6 +349,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -429,6 +432,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -460,6 +464,7 @@ function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, @@ -523,6 +528,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, 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 bd2f96609af6..ad3ff1b4004c 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -101,7 +101,7 @@ describe('ReactFlightDOMNode', () => { return ( ' in ' + name + - (/\d/.test(m) + (/:\d+:\d+/.test(m) ? preserveLocation ? ' ' + location.replace(__filename, relativeFilename) : ' (at **)' @@ -1589,6 +1589,296 @@ describe('ReactFlightDOMNode', () => { expect(ownerStack).toBeNull(); } }); + + function createReadableWithLateRelease(initialChunks, lateChunks, signal) { + // Create a new Readable and push all initial chunks immediately. + const readable = new Stream.Readable({...streamOptions, read() {}}); + for (let i = 0; i < initialChunks.length; i++) { + readable.push(initialChunks[i]); + } + + // When prerendering is aborted, push all dynamic chunks. They won't be + // considered for rendering, but they include debug info we want to use. + signal.addEventListener( + 'abort', + () => { + for (let i = 0; i < lateChunks.length; i++) { + readable.push(lateChunks[i]); + } + setImmediate(() => { + readable.push(null); + }); + }, + {once: true}, + ); + + return readable; + } + + async function reencodeFlightStream( + staticChunks, + dynamicChunks, + startTime, + serverConsumerManifest, + ) { + let staticEndTime = -1; + const chunks = { + static: [], + dynamic: [], + }; + await new Promise(async resolve => { + const renderStageController = new AbortController(); + + const serverStream = createReadableWithLateRelease( + staticChunks, + dynamicChunks, + renderStageController.signal, + ); + const decoded = await ReactServerDOMClient.createFromNodeStream( + serverStream, + serverConsumerManifest, + { + // We're re-encoding the whole stream, so we don't want to filter out any debug info. + endTime: undefined, + }, + ); + + setTimeout(async () => { + const stream = ReactServerDOMServer.renderToPipeableStream( + decoded, + webpackMap, + { + filterStackFrame, + // Pass in the original render's startTime to avoid omitting its IO info. + startTime, + }, + ); + + const passThrough = new Stream.PassThrough(streamOptions); + + passThrough.on('data', chunk => { + if (!renderStageController.signal.aborted) { + chunks.static.push(chunk); + } else { + chunks.dynamic.push(chunk); + } + }); + passThrough.on('end', resolve); + + stream.pipe(passThrough); + }); + + setTimeout(() => { + staticEndTime = performance.now() + performance.timeOrigin; + renderStageController.abort(); + }); + }); + + return {chunks, staticEndTime}; + } + + // @gate __DEV__ + it('can preserve old IO info when decoding and re-encoding a stream with options.startTime', async () => { + let resolveDynamicData; + + function getDynamicData() { + return new Promise(resolve => { + resolveDynamicData = resolve; + }); + } + + async function Dynamic() { + const data = await getDynamicData(); + return ReactServer.createElement('p', null, data); + } + + function App() { + return ReactServer.createElement( + 'html', + null, + ReactServer.createElement( + 'body', + null, + ReactServer.createElement( + ReactServer.Suspense, + {fallback: 'Loading...'}, + // TODO: having a wrapper
here seems load-bearing. + // ReactServer.createElement(ReactServer.createElement(Dynamic)), + ReactServer.createElement( + 'section', + null, + ReactServer.createElement(Dynamic), + ), + ), + ), + ); + } + + const resolveDynamic = () => { + resolveDynamicData('Hi Janka'); + }; + + // 1. Render , dividing the output into static and dynamic content. + + let startTime = -1; + + let isStatic = true; + const chunks1 = { + static: [], + dynamic: [], + }; + + await new Promise(resolve => { + setTimeout(async () => { + startTime = performance.now() + performance.timeOrigin; + + const stream = ReactServerDOMServer.renderToPipeableStream( + ReactServer.createElement(App), + webpackMap, + { + filterStackFrame, + startTime, + environmentName() { + return isStatic ? 'Prerender' : 'Server'; + }, + }, + ); + + const passThrough = new Stream.PassThrough(streamOptions); + + passThrough.on('data', chunk => { + if (isStatic) { + chunks1.static.push(chunk); + } else { + chunks1.dynamic.push(chunk); + } + }); + passThrough.on('end', resolve); + + stream.pipe(passThrough); + }); + setTimeout(() => { + isStatic = false; + resolveDynamic(); + }); + }); + + //=============================================== + // 2. Decode the stream from the previous step and render it again. + // This should preserve existing debug info. + + const serverConsumerManifest = { + moduleMap: null, + moduleLoading: null, + }; + + const {chunks: chunks2, staticEndTime: reencodeStaticEndTime} = + await reencodeFlightStream( + chunks1.static, + chunks1.dynamic, + // This is load-bearing. If we don't pass a startTime, IO info + // from the initial render will be skipped (because it finished in the past) + // and we won't get the precise location of the blocking await in the owner stack. + startTime, + serverConsumerManifest, + ); + + //=============================================== + // 3. SSR the stream from the previous step and abort it after the static stage + // (which should trigger `onError` for each "hole" that hasn't resolved yet) + + function ClientRoot({response}) { + return use(response); + } + + let ssrStream; + let ownerStack; + let componentStack; + + await new Promise(async (resolve, reject) => { + const renderController = new AbortController(); + + const serverStream = createReadableWithLateRelease( + chunks2.static, + chunks2.dynamic, + renderController.signal, + ); + + const decodedPromise = ReactServerDOMClient.createFromNodeStream( + serverStream, + serverConsumerManifest, + { + endTime: reencodeStaticEndTime, + }, + ); + + setTimeout(() => { + ssrStream = ReactDOMServer.renderToPipeableStream( + React.createElement(ClientRoot, { + response: decodedPromise, + }), + { + onError(err, errorInfo) { + componentStack = errorInfo.componentStack; + ownerStack = React.captureOwnerStack + ? React.captureOwnerStack() + : null; + return null; + }, + }, + ); + + renderController.signal.addEventListener( + 'abort', + () => { + const {reason} = renderController.signal; + ssrStream.abort(reason); + }, + { + once: true, + }, + ); + }); + + setTimeout(() => { + renderController.abort(new Error('ssr-abort')); + resolve(); + }); + }); + + const result = await readResult(ssrStream); + + expect(normalizeCodeLocInfo(componentStack)).toBe( + '\n' + + // TODO: + // when we reencode a stream, the component stack doesn't have server frames for the dynamic content + // (which is what causes the dynamic hole here) + // because Flight delays forwarding debug info for lazies until they resolve. + // (the owner stack is filled in `pushHaltedAwaitOnComponentStack`, so it works fine) + // + // ' in Dynamic (at **)\n' + ' in section\n' + + ' in Suspense\n' + + ' in body\n' + + ' in html\n' + + ' in App (at **)\n' + + ' in ClientRoot (at **)', + ); + expect(normalizeCodeLocInfo(ownerStack)).toBe( + '\n' + + gate(flags => + flags.enableAsyncDebugInfo + ? ' in Dynamic (at **)\n' + : ' in section\n', + ) + + ' in App (at **)', + ); + + expect(result).toContain( + 'Switched to client rendering because the server rendering aborted due to:\n\n' + + 'ssr-abort', + ); + }); }); it('warns with a tailored message if eval is not available in dev', async () => { diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js index 1c417ff6bda3..d1d0772186e6 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js @@ -64,6 +64,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -126,6 +127,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -214,6 +216,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js index 77067754bc59..1347b07dd4df 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js @@ -69,6 +69,7 @@ type Options = { signal?: AbortSignal, temporaryReferences?: TemporaryReferenceSet, onError?: (error: mixed) => void, + startTime?: number, }; function startReadingFromDebugChannelReadableStream( @@ -131,6 +132,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -219,6 +221,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js index 888d01391449..e710eafd00a1 100644 --- a/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js @@ -152,6 +152,7 @@ type Options = { onError?: (error: mixed) => void, identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, + startTime?: number, }; type PipeableStream = { @@ -189,6 +190,7 @@ function renderToPipeableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -347,6 +349,7 @@ function renderToReadableStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, debugChannelReadable !== undefined, @@ -429,6 +432,7 @@ type PrerenderOptions = { identifierPrefix?: string, temporaryReferences?: TemporaryReferenceSet, signal?: AbortSignal, + startTime?: number, }; type StaticResult = { @@ -460,6 +464,7 @@ function prerenderToNodeStream( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, @@ -523,6 +528,7 @@ function prerender( options ? options.onError : undefined, options ? options.identifierPrefix : undefined, options ? options.temporaryReferences : undefined, + options ? options.startTime : undefined, __DEV__ && options ? options.environmentName : undefined, __DEV__ && options ? options.filterStackFrame : undefined, false, diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index f31fa45f7a77..6edfbaa4df3e 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -660,6 +660,7 @@ function RequestInstance( onFatalError: (error: mixed) => void, identifierPrefix?: string, temporaryReferences: void | TemporaryReferenceSet, + debugStartTime: void | number, // Profiling-only environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only keepDebugAlive: boolean, // DEV-only @@ -751,7 +752,15 @@ function RequestInstance( // This avoids leaking unnecessary information like how long the server has // been running and allows for more compact representation of each timestamp. // The time origin is stored as an offset in the time space of this environment. - timeOrigin = this.timeOrigin = performance.now(); + if (typeof debugStartTime === 'number') { + // We expect `startTime` to be an absolute timestamp, so relativize it to match the other case. + timeOrigin = this.timeOrigin = + debugStartTime - + // $FlowFixMe[prop-missing] + performance.timeOrigin; + } else { + timeOrigin = this.timeOrigin = performance.now(); + } emitTimeOriginChunk( this, timeOrigin + @@ -784,6 +793,7 @@ export function createRequest( onError: void | ((error: mixed) => ?string), identifierPrefix: void | string, temporaryReferences: void | TemporaryReferenceSet, + debugStartTime: void | number, // Profiling-only environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only keepDebugAlive: boolean, // DEV-only @@ -802,6 +812,7 @@ export function createRequest( noop, identifierPrefix, temporaryReferences, + debugStartTime, environmentName, filterStackFrame, keepDebugAlive, @@ -816,6 +827,7 @@ export function createPrerenderRequest( onError: void | ((error: mixed) => ?string), identifierPrefix: void | string, temporaryReferences: void | TemporaryReferenceSet, + debugStartTime: void | number, // Profiling-only environmentName: void | string | (() => string), // DEV-only filterStackFrame: void | ((url: string, functionName: string) => boolean), // DEV-only keepDebugAlive: boolean, // DEV-only @@ -834,6 +846,7 @@ export function createPrerenderRequest( onFatalError, identifierPrefix, temporaryReferences, + debugStartTime, environmentName, filterStackFrame, keepDebugAlive, From 3419420e8b8415c80ead82e4d95f0a039380aee5 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Tue, 3 Feb 2026 16:08:18 +0100 Subject: [PATCH 3/6] [flags] Cleanup `enableActivity` (#35681) --- .../src/__tests__/ReactDOMActivity-test.js | 7 ---- .../__tests__/ReactDOMFragmentRefs-test.js | 6 +-- ...DOMServerPartialHydration-test.internal.js | 2 - ...rPartialHydrationActivity-test.internal.js | 41 ++----------------- ...electiveHydrationActivity-test.internal.js | 26 +++--------- .../src/__tests__/Activity-test.js | 23 ----------- .../__tests__/ActivityErrorHandling-test.js | 1 - .../__tests__/ActivityLegacySuspense-test.js | 8 +--- .../src/__tests__/ActivityStrictMode-test.js | 6 +-- .../src/__tests__/ActivitySuspense-test.js | 8 +--- .../src/__tests__/ReactDeferredValue-test.js | 5 --- .../src/__tests__/ReactErrorStacks-test.js | 1 - .../ReactHooksWithNoopRenderer-test.js | 1 - .../src/__tests__/ReactLazy-test.internal.js | 1 - .../ReactSiblingPrerendering-test.js | 1 - .../ReactSuspenseyCommitPhase-test.js | 2 +- .../__tests__/ReactFreshIntegration-test.js | 18 ++++---- 17 files changed, 27 insertions(+), 130 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMActivity-test.js b/packages/react-dom/src/__tests__/ReactDOMActivity-test.js index ec2048ec434f..abaca64da260 100644 --- a/packages/react-dom/src/__tests__/ReactDOMActivity-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMActivity-test.js @@ -54,7 +54,6 @@ describe('ReactDOMActivity', () => { return {props.children}; } - // @gate enableActivity it( 'hiding an Activity boundary also hides the direct children of any ' + 'portals it contains, regardless of how deeply nested they are', @@ -100,7 +99,6 @@ describe('ReactDOMActivity', () => { }, ); - // @gate enableActivity it( 'revealing an Activity boundary inside a portal does not reveal the ' + 'portal contents if has a hidden Activity parent', @@ -151,7 +149,6 @@ describe('ReactDOMActivity', () => { }, ); - // @gate enableActivity it('hides new portals added to an already hidden tree', async () => { function Child() { return ; @@ -218,7 +215,6 @@ describe('ReactDOMActivity', () => { ); }); - // @gate enableActivity it('hides new insertions inside an already hidden portal', async () => { function Child({text}) { useLayoutEffect(() => { @@ -289,7 +285,6 @@ describe('ReactDOMActivity', () => { ); }); - // @gate enableActivity it('reveal an inner Suspense boundary without revealing an outer Activity on the same host child', async () => { const promise = new Promise(() => {}); @@ -390,7 +385,6 @@ describe('ReactDOMActivity', () => { ); }); - // @gate enableActivity it('mounts/unmounts layout effects in portal when visibility changes (starting visible)', async () => { function Child() { useLayoutEffect(() => { @@ -440,7 +434,6 @@ describe('ReactDOMActivity', () => { ); }); - // @gate enableActivity it('mounts/unmounts layout effects in portal when visibility changes (starting hidden)', async () => { function Child() { useLayoutEffect(() => { diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js index 7ff088d7d312..603af9072b6e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js @@ -763,7 +763,7 @@ describe('FragmentRefs', () => { }); describe('with activity', () => { - // @gate enableFragmentRefs && enableActivity + // @gate enableFragmentRefs it('does not apply event listeners to hidden trees', async () => { const parentRef = React.createRef(); const fragmentRef = React.createRef(); @@ -799,7 +799,7 @@ describe('FragmentRefs', () => { expect(logs).toEqual(['Child 1', 'Child 3']); }); - // @gate enableFragmentRefs && enableActivity + // @gate enableFragmentRefs it('applies event listeners to visible trees', async () => { const parentRef = React.createRef(); const fragmentRef = React.createRef(); @@ -835,7 +835,7 @@ describe('FragmentRefs', () => { expect(logs).toEqual(['Child 1', 'Child 2', 'Child 3']); }); - // @gate enableFragmentRefs && enableActivity + // @gate enableFragmentRefs it('handles Activity modes switching', async () => { const fragmentRef = React.createRef(); const fragmentRef2 = React.createRef(); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 502ac8c326c1..3e6af7bc9744 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -3668,7 +3668,6 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current.innerHTML).toBe('Hidden child'); }); - // @gate enableActivity it('a visible Activity component is surrounded by comment markers', async () => { const ref = React.createRef(); @@ -3706,7 +3705,6 @@ describe('ReactDOMServerPartialHydration', () => { expect(ref.current).toBe(span); }); - // @gate enableActivity it('a hidden Activity component is skipped over during server rendering', async () => { const visibleRef = React.createRef(); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydrationActivity-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydrationActivity-test.internal.js index cf9e5d1d0399..0921920cb55d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydrationActivity-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydrationActivity-test.internal.js @@ -113,7 +113,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { IdleEventPriority = require('react-reconciler/constants').IdleEventPriority; }); - // @gate enableActivity it('hydrates a parent even if a child Activity boundary is blocked', async () => { let suspend = false; let resolve; @@ -169,7 +168,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(ref.current).toBe(span); }); - // @gate enableActivity it('can hydrate siblings of a suspended component without errors', async () => { let suspend = false; let resolve; @@ -228,7 +226,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.textContent).toBe('HelloHello'); }); - // @gate enableActivity it('falls back to client rendering boundary on mismatch', async () => { let client = false; let suspend = false; @@ -321,7 +318,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { ); }); - // @gate enableActivity it('handles if mismatch is after suspending', async () => { let client = false; let suspend = false; @@ -401,7 +397,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.innerHTML).toBe('Hello
Mismatch
'); }); - // @gate enableActivity it('handles if mismatch is child of suspended component', async () => { let client = false; let suspend = false; @@ -482,7 +477,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.innerHTML).toBe('
Mismatch
'); }); - // @gate enableActivity it('handles if mismatch is parent and first child suspends', async () => { let client = false; let suspend = false; @@ -575,7 +569,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { ); }); - // @gate enableActivity it('does show a parent fallback if mismatch is parent and second child suspends', async () => { let client = false; let suspend = false; @@ -687,7 +680,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { ); }); - // @gate enableActivity it('does show a parent fallback if mismatch is in parent element only', async () => { let client = false; let suspend = false; @@ -787,7 +779,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { ); }); - // @gate enableActivity it('does show a parent fallback if mismatch is before suspending', async () => { let client = false; let suspend = false; @@ -886,7 +877,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { ); }); - // @gate enableActivity it('does show a parent fallback if mismatch is before suspending in a child', async () => { let client = false; let suspend = false; @@ -987,7 +977,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { ); }); - // @gate enableActivity it('calls the hydration callbacks after hydration or deletion', async () => { let suspend = false; let resolve; @@ -1079,7 +1068,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(deleted.length).toBe(1); }); - // @gate enableActivity it('hydrates an empty activity boundary', async () => { function App() { return ( @@ -1101,7 +1089,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.innerHTML).toContain('
Sibling
'); }); - // @gate enableActivity it('recovers with client render when server rendered additional nodes at suspense root', async () => { function CheckIfHydrating({children}) { // This is a trick to check whether we're hydrating or not, since React @@ -1171,7 +1158,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(ref.current).not.toBe(span); }); - // @gate enableActivity it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => { const ref = React.createRef(); let shouldSuspend = false; @@ -1236,7 +1222,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(ref.current).not.toBe(span); }); - // @gate enableActivity it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => { const ref = React.createRef(); function App({hasB}) { @@ -1283,7 +1268,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(ref.current).not.toBe(span); }); - // @gate enableActivity it('calls the onDeleted hydration callback if the parent gets deleted', async () => { let suspend = false; const promise = new Promise(() => {}); @@ -1337,7 +1321,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(deleted.length).toBe(1); }); - // @gate enableActivity it('can insert siblings before the dehydrated boundary', async () => { let suspend = false; const promise = new Promise(() => {}); @@ -1395,7 +1378,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.firstChild.firstChild.textContent).toBe('First'); }); - // @gate enableActivity it('can delete the dehydrated boundary before it is hydrated', async () => { let suspend = false; const promise = new Promise(() => {}); @@ -1451,7 +1433,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.firstChild.children[1].textContent).toBe('After'); }); - // @gate enableActivity it('blocks updates to hydrate the content first if props have changed', async () => { let suspend = false; let resolve; @@ -1523,7 +1504,7 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(span.className).toBe('hi'); }); - // @gate enableActivity && www + // @gate www it('blocks updates to hydrate the content first if props changed at idle priority', async () => { let suspend = false; let resolve; @@ -1597,7 +1578,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(span.className).toBe('hi'); }); - // @gate enableActivity it('shows the fallback of the parent if props have changed before hydration completes and is still suspended', async () => { let suspend = false; let resolve; @@ -1684,7 +1664,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.textContent).toBe('Hi'); }); - // @gate enableActivity it('clears nested activity boundaries if they did not hydrate yet', async () => { let suspend = false; const promise = new Promise(() => {}); @@ -1752,7 +1731,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.textContent).toBe('Hi Hi'); }); - // @gate enableActivity it('hydrates first if props changed but we are able to resolve within a timeout', async () => { let suspend = false; let resolve; @@ -1826,7 +1804,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(span.className).toBe('hi'); }); - // @gate enableActivity it('warns but works if setState is called before commit in a dehydrated component', async () => { let suspend = false; let resolve; @@ -1901,7 +1878,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.textContent).toBe('Hello'); }); - // @gate enableActivity it('blocks the update to hydrate first if context has changed', async () => { let suspend = false; let resolve; @@ -1984,7 +1960,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(span.className).toBe('hi'); }); - // @gate enableActivity it('shows the parent fallback if context has changed before hydration completes and is still suspended', async () => { let suspend = false; let resolve; @@ -2075,7 +2050,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(container.textContent).toBe('Hi'); }); - // @gate enableActivity it('can hydrate TWO activity boundaries', async () => { const ref1 = React.createRef(); const ref2 = React.createRef(); @@ -2113,7 +2087,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(ref2.current).toBe(span2); }); - // @gate enableActivity it('regenerates if it cannot hydrate before changes to props/context expire', async () => { let suspend = false; const promise = new Promise(resolvePromise => {}); @@ -2201,7 +2174,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(newSpan.className).toBe('hi'); }); - // @gate enableActivity it('does not invoke an event on a hydrated node until it commits', async () => { let suspend = false; let resolve; @@ -2281,7 +2253,7 @@ describe('ReactDOMServerPartialHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity && www + // @gate www it('does not invoke an event on a hydrated event handle until it commits', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -2363,7 +2335,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('invokes discrete events on nested activity boundaries in a root (legacy system)', async () => { let suspend = false; let resolve; @@ -2444,7 +2415,7 @@ describe('ReactDOMServerPartialHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity && www + // @gate www it('invokes discrete events on nested activity boundaries in a root (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -2529,7 +2500,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('does not invoke the parent of dehydrated boundary event', async () => { let suspend = false; let resolve; @@ -2602,7 +2572,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('does not invoke an event on a parent tree when a subtree is dehydrated', async () => { let suspend = false; let resolve; @@ -2675,7 +2644,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { document.body.removeChild(parentContainer); }); - // @gate enableActivity it('blocks only on the last continuous event (legacy system)', async () => { let suspend1 = false; let resolve1; @@ -2776,7 +2744,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('finishes normal pri work before continuing to hydrate a retry', async () => { let suspend = false; let resolve; @@ -2860,7 +2827,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { expect(ref.current).not.toBe(null); }); - // @gate enableActivity it('regression test: does not overfire non-bubbling browser events', async () => { let suspend = false; let resolve; @@ -2945,7 +2911,6 @@ describe('ReactDOMServerPartialHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('fallback to client render on hydration mismatch at root', async () => { let suspend = true; let resolve; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydrationActivity-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydrationActivity-test.internal.js index c2a856a01818..1892b915be8e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydrationActivity-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydrationActivity-test.internal.js @@ -152,7 +152,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { require('react-reconciler/constants').ContinuousEventPriority; }); - // @gate enableActivity it('hydrates the target boundary synchronously during a click', async () => { function Child({text}) { Scheduler.log(text); @@ -214,7 +213,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('hydrates at higher pri if sync did not work first time', async () => { let suspend = false; let resolve; @@ -300,7 +298,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('hydrates at higher pri for secondary discrete events', async () => { let suspend = false; let resolve; @@ -388,7 +385,7 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity && www + // @gate www it('hydrates the target boundary synchronously during a click (createEventHandle)', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let isServerRendering = true; @@ -455,7 +452,7 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity && www + // @gate www it('hydrates at higher pri if sync did not work first time (createEventHandle)', async () => { let suspend = false; let isServerRendering = true; @@ -544,7 +541,7 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity && www + // @gate www it('hydrates at higher pri for secondary discrete events (createEventHandle)', async () => { const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; @@ -636,7 +633,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('hydrates the hovered targets as higher priority for continuous events', async () => { let suspend = false; let resolve; @@ -729,7 +725,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('replays capture phase for continuous events and respects stopPropagation', async () => { let suspend = false; let resolve; @@ -883,7 +878,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('replays event with null target when tree is dismounted', async () => { let suspend = false; let resolve; @@ -957,7 +951,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('hydrates the last target path first for continuous events', async () => { let suspend = false; let resolve; @@ -1044,7 +1037,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('hydrates the last explicitly hydrated target at higher priority', async () => { function Child({text}) { Scheduler.log(text); @@ -1092,7 +1084,7 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { await waitForAll(['App', 'C', 'B', 'A']); }); - // @gate enableActivity && www + // @gate www it('hydrates before an update even if hydration moves away from it', async () => { function Child({text}) { Scheduler.log(text); @@ -1200,7 +1192,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('fires capture event handlers and native events if content is hydratable during discrete event', async () => { spyOnDev(console, 'error'); function Child({text}) { @@ -1272,7 +1263,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { document.body.removeChild(container); }); - // @gate enableActivity it('does not propagate discrete event if it cannot be synchronously hydrated', async () => { let triggeredParent = false; let triggeredChild = false; @@ -1335,7 +1325,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { expect(triggeredChild).toBe(false); }); - // @gate enableActivity it('can force hydration in response to sync update', async () => { function Child({text}) { Scheduler.log(`Child ${text}`); @@ -1371,7 +1360,7 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { expect(initialSpan).toBe(spanRef); }); - // @gate enableActivity && www + // @gate www it('can force hydration in response to continuous update', async () => { function Child({text}) { Scheduler.log(`Child ${text}`); @@ -1408,7 +1397,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { expect(initialSpan).toBe(spanRef); }); - // @gate enableActivity it('can force hydration in response to default update', async () => { function Child({text}) { Scheduler.log(`Child ${text}`); @@ -1441,7 +1429,7 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { expect(initialSpan).toBe(spanRef); }); - // @gate enableActivity && www + // @gate www it('regression test: can unwind context on selective hydration interruption', async () => { const Context = React.createContext('DefaultContext'); @@ -1500,7 +1488,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { }); }); - // @gate enableActivity it('regression test: can unwind context on selective hydration interruption for sync updates', async () => { const Context = React.createContext('DefaultContext'); @@ -1552,7 +1539,6 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { }); }); - // @gate enableActivity it('regression: selective hydration does not contribute to "maximum update limit" count', async () => { const outsideRef = React.createRef(null); const insideRef = React.createRef(null); diff --git a/packages/react-reconciler/src/__tests__/Activity-test.js b/packages/react-reconciler/src/__tests__/Activity-test.js index 2fdcc16701db..a27e62e585fb 100644 --- a/packages/react-reconciler/src/__tests__/Activity-test.js +++ b/packages/react-reconciler/src/__tests__/Activity-test.js @@ -196,7 +196,6 @@ describe('Activity', () => { ); }); - // @gate enableActivity it('mounts without layout effects when hidden', async () => { function Child({text}) { useLayoutEffect(() => { @@ -234,7 +233,6 @@ describe('Activity', () => { expect(root).toMatchRenderedOutput(); }); - // @gate enableActivity it('mounts/unmounts layout effects when visibility changes (starting visible)', async () => { function Child({text}) { useLayoutEffect(() => { @@ -280,7 +278,6 @@ describe('Activity', () => { expect(root).toMatchRenderedOutput(); }); - // @gate enableActivity it('nested offscreen does not call componentWillUnmount when hidden', async () => { // This is a bug that appeared during production test of . // It is a very specific scenario with nested Offscreens. The inner offscreen @@ -384,7 +381,6 @@ describe('Activity', () => { assertLog(['child']); }); - // @gate enableActivity it('mounts/unmounts layout effects when visibility changes (starting hidden)', async () => { function Child({text}) { useLayoutEffect(() => { @@ -431,7 +427,6 @@ describe('Activity', () => { expect(root).toMatchRenderedOutput(