diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js
index 0bfe0fe630c4..9f8deed495d3 100644
--- a/fixtures/flight/src/App.js
+++ b/fixtures/flight/src/App.js
@@ -2,7 +2,7 @@ import * as React from 'react';
import {renderToReadableStream} from 'react-server-dom-unbundled/server';
import {createFromReadableStream} from 'react-server-dom-webpack/client';
import {PassThrough, Readable} from 'stream';
-
+import {ClientContext, ClientReadContext} from './ClientContext.js';
import Container from './Container.js';
import {Counter} from './Counter.js';
@@ -235,6 +235,11 @@ export default async function App({prerender, noCache}) {
{dedupedChild}
{Promise.resolve([dedupedChild])}
+
+
+
+
+
{prerender ? null : ( // TODO: prerender is broken for large content for some reason.
diff --git a/fixtures/flight/src/ClientContext.js b/fixtures/flight/src/ClientContext.js
new file mode 100644
index 000000000000..7d9340e62a8a
--- /dev/null
+++ b/fixtures/flight/src/ClientContext.js
@@ -0,0 +1,12 @@
+'use client';
+
+import {createContext, use} from 'react';
+
+const ClientContext = createContext(null);
+
+function ClientReadContext() {
+ const value = use(ClientContext);
+ return {value}
;
+}
+
+export {ClientContext, ClientReadContext};
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) => {
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('HelloMismatch');
});
- // @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('');
});
- // @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-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-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 4ea1a1526370..48ced588730b 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -128,6 +128,7 @@ import {
REACT_LAZY_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_MEMO_TYPE,
+ REACT_CONTEXT_TYPE,
} from 'shared/ReactSymbols';
import {setCurrentFiber} from './ReactCurrentFiber';
import {
@@ -2140,6 +2141,10 @@ function mountLazyComponent(
props,
renderLanes,
);
+ } else if ($$typeof === REACT_CONTEXT_TYPE) {
+ workInProgress.tag = ContextProvider;
+ workInProgress.type = Component;
+ return updateContextProvider(null, workInProgress, renderLanes);
}
}
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();
});
- // @gate enableActivity
it('hides children of offscreen after layout effects are destroyed', async () => {
const root = ReactNoop.createRoot();
function Child({text}) {
@@ -518,7 +513,6 @@ describe('Activity', () => {
assertLog(['Unmount layout']);
});
- // @gate enableActivity
it('hides new insertions into an already hidden tree', async () => {
const root = ReactNoop.createRoot();
await act(() => {
@@ -548,7 +542,6 @@ describe('Activity', () => {
);
});
- // @gate enableActivity
it('hides updated nodes inside an already hidden tree', async () => {
const root = ReactNoop.createRoot();
await act(() => {
@@ -594,7 +587,6 @@ describe('Activity', () => {
expect(root).toMatchRenderedOutput(Hi);
});
- // @gate enableActivity
it('revealing a hidden tree at high priority does not cause tearing', async () => {
// When revealing an offscreen tree, we need to include updates that were
// previously deferred because the tree was hidden, even if they are lower
@@ -723,7 +715,6 @@ describe('Activity', () => {
expect(areOuterAndInnerConsistent()).toBe(true);
});
- // @gate enableActivity
it('regression: Activity instance is sometimes null during setState', async () => {
let setState;
function Child() {
@@ -774,7 +765,6 @@ describe('Activity', () => {
expect(root).toMatchRenderedOutput(null);
});
- // @gate enableActivity
it('class component setState callbacks do not fire until tree is visible', async () => {
const root = ReactNoop.createRoot();
@@ -826,7 +816,6 @@ describe('Activity', () => {
expect(root).toMatchRenderedOutput();
});
- // @gate enableActivity
it('does not call componentDidUpdate when reappearing a hidden class component', async () => {
class Child extends React.Component {
componentDidMount() {
@@ -876,7 +865,6 @@ describe('Activity', () => {
assertLog(['componentDidMount']);
});
- // @gate enableActivity
it(
'when reusing old components (hidden -> visible), layout effects fire ' +
'with same timing as if it were brand new',
@@ -929,7 +917,6 @@ describe('Activity', () => {
},
);
- // @gate enableActivity
it(
'when reusing old components (hidden -> visible), layout effects fire ' +
'with same timing as if it were brand new (includes setState callback)',
@@ -992,7 +979,6 @@ describe('Activity', () => {
},
);
- // @gate enableActivity
it('defer passive effects when prerendering a new Activity tree', async () => {
function Child({label}) {
useEffect(() => {
@@ -1110,7 +1096,6 @@ describe('Activity', () => {
assertLog(['Shell', 'More']);
});
- // @gate enableActivity
it('passive effects are connected and disconnected when the visibility changes', async () => {
function Child({step}) {
useEffect(() => {
@@ -1167,7 +1152,6 @@ describe('Activity', () => {
expect(root).toMatchRenderedOutput();
});
- // @gate enableActivity
it('passive effects are unmounted on hide in the same order as during a deletion: parent before child', async () => {
function Child({label}) {
useEffect(() => {
@@ -1225,7 +1209,6 @@ describe('Activity', () => {
// Re-enable this test once we add this ability. For example, we'll likely add
// either an option or a heuristic to mount passive effects inside a hidden
// tree after a delay.
- // @gate enableActivity
// eslint-disable-next-line jest/no-disabled-tests
it.skip("don't defer passive effects when prerendering in a tree whose effects are already connected", async () => {
function Child({label}) {
@@ -1282,7 +1265,6 @@ describe('Activity', () => {
]);
});
- // @gate enableActivity
it('does not mount effects when prerendering a nested Activity boundary', async () => {
function Child({label}) {
useEffect(() => {
@@ -1360,7 +1342,6 @@ describe('Activity', () => {
);
});
- // @gate enableActivity
it('reveal an outer Activity boundary without revealing an inner one', async () => {
function Child({label}) {
useEffect(() => {
@@ -1426,7 +1407,6 @@ describe('Activity', () => {
);
});
- // @gate enableActivity
it('reveal an inner Activity boundary without revealing an outer one on the same host child', async () => {
// This ensures that no update is scheduled, which would cover up the bug if the parent
// then re-hides the child on the way up.
@@ -1455,7 +1435,6 @@ describe('Activity', () => {
expect(root).toMatchRenderedOutput();
});
- // @gate enableActivity
it('reveal an inner Suspense boundary without revealing an outer Activity on the same host child', async () => {
// This ensures that no update is scheduled, which would cover up the bug if the parent
// then re-hides the child on the way up.
@@ -1492,7 +1471,6 @@ describe('Activity', () => {
expect(root).toMatchRenderedOutput();
});
- // @gate enableActivity
it('insertion effects are not disconnected when the visibility changes', async () => {
function Child({step}) {
useInsertionEffect(() => {
@@ -1549,7 +1527,6 @@ describe('Activity', () => {
expect(root).toMatchRenderedOutput();
});
- // @gate enableActivity
it('warns if you pass a hidden prop', async () => {
function App() {
return (
diff --git a/packages/react-reconciler/src/__tests__/ActivityErrorHandling-test.js b/packages/react-reconciler/src/__tests__/ActivityErrorHandling-test.js
index ac6d1f9b88b4..af22b95f5472 100644
--- a/packages/react-reconciler/src/__tests__/ActivityErrorHandling-test.js
+++ b/packages/react-reconciler/src/__tests__/ActivityErrorHandling-test.js
@@ -26,7 +26,6 @@ describe('Activity error handling', () => {
return text;
}
- // @gate enableActivity
it(
'errors inside a hidden Activity do not escape in the visible part ' +
'of the UI',
diff --git a/packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js b/packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js
index a4ec2298174b..482aa54ff99f 100644
--- a/packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js
+++ b/packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js
@@ -98,7 +98,6 @@ describe('Activity Suspense', () => {
return text;
}
- // @gate enableActivity
it('basic example of suspending inside hidden tree', async () => {
const root = ReactNoop.createRoot();
@@ -174,7 +173,7 @@ describe('Activity Suspense', () => {
);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
test('Regression: Suspending on hide should not infinite loop.', async () => {
// This regression only repros in public act.
global.IS_REACT_ACT_ENVIRONMENT = true;
@@ -223,7 +222,6 @@ describe('Activity Suspense', () => {
expect(root).toMatchRenderedOutput('');
});
- // @gate enableActivity
test("suspending inside currently hidden tree that's switching to visible", async () => {
const root = ReactNoop.createRoot();
@@ -282,7 +280,6 @@ describe('Activity Suspense', () => {
);
});
- // @gate enableActivity
test("suspending inside currently visible tree that's switching to hidden", async () => {
const root = ReactNoop.createRoot();
@@ -368,7 +365,6 @@ describe('Activity Suspense', () => {
);
});
- // @gate enableActivity
test('update that suspends inside hidden tree', async () => {
let setText;
function Child() {
@@ -401,7 +397,6 @@ describe('Activity Suspense', () => {
});
});
- // @gate enableActivity
test('updates at multiple priorities that suspend inside hidden tree', async () => {
let setText;
let setStep;
@@ -458,7 +453,6 @@ describe('Activity Suspense', () => {
expect(root).toMatchRenderedOutput(B1);
});
- // @gate enableActivity
test('detect updates to a hidden tree during a concurrent event', async () => {
// This is a pretty complex test case. It relates to how we detect if an
// update is made to a hidden tree: when scheduling the update, we walk up
diff --git a/packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js b/packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js
index f8f7fca0e09f..fa67df3b8a1e 100644
--- a/packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js
+++ b/packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js
@@ -31,7 +31,7 @@ describe('Activity StrictMode', () => {
return label;
}
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('should trigger strict effects when offscreen is visible', async () => {
await act(() => {
ReactNoop.render(
@@ -55,7 +55,7 @@ describe('Activity StrictMode', () => {
]);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('should not trigger strict effects when offscreen is hidden', async () => {
await act(() => {
ReactNoop.render(
@@ -154,7 +154,7 @@ describe('Activity StrictMode', () => {
});
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('should double invoke effects on unsuspended child', async () => {
let shouldSuspend = true;
let resolve;
diff --git a/packages/react-reconciler/src/__tests__/ActivitySuspense-test.js b/packages/react-reconciler/src/__tests__/ActivitySuspense-test.js
index 90fdf21a4286..3a783ff32671 100644
--- a/packages/react-reconciler/src/__tests__/ActivitySuspense-test.js
+++ b/packages/react-reconciler/src/__tests__/ActivitySuspense-test.js
@@ -95,7 +95,6 @@ describe('Activity Suspense', () => {
return text;
}
- // @gate enableActivity
it('basic example of suspending inside hidden tree', async () => {
const root = ReactNoop.createRoot();
@@ -171,7 +170,7 @@ describe('Activity Suspense', () => {
);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
test('Regression: Suspending on hide should not infinite loop.', async () => {
// This regression only repros in public act.
global.IS_REACT_ACT_ENVIRONMENT = true;
@@ -220,7 +219,6 @@ describe('Activity Suspense', () => {
expect(root).toMatchRenderedOutput('');
});
- // @gate enableActivity
test("suspending inside currently hidden tree that's switching to visible", async () => {
const root = ReactNoop.createRoot();
@@ -289,7 +287,6 @@ describe('Activity Suspense', () => {
);
});
- // @gate enableActivity
test("suspending inside currently visible tree that's switching to hidden", async () => {
const root = ReactNoop.createRoot();
@@ -380,7 +377,6 @@ describe('Activity Suspense', () => {
);
});
- // @gate enableActivity
test('update that suspends inside hidden tree', async () => {
let setText;
function Child() {
@@ -413,7 +409,6 @@ describe('Activity Suspense', () => {
});
});
- // @gate enableActivity
test('updates at multiple priorities that suspend inside hidden tree', async () => {
let setText;
let setStep;
@@ -470,7 +465,6 @@ describe('Activity Suspense', () => {
expect(root).toMatchRenderedOutput(B1);
});
- // @gate enableActivity
test('detect updates to a hidden tree during a concurrent event', async () => {
// This is a pretty complex test case. It relates to how we detect if an
// update is made to a hidden tree: when scheduling the update, we walk up
diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
index ad07ce89589f..e4fe796fd353 100644
--- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
@@ -696,7 +696,6 @@ describe('ReactDeferredValue', () => {
expect(root).toMatchRenderedOutput('Content');
});
- // @gate enableActivity
it('useDeferredValue can spawn a deferred task while prerendering a hidden tree', async () => {
function App() {
const text = useDeferredValue('Final', 'Preview');
@@ -742,7 +741,6 @@ describe('ReactDeferredValue', () => {
expect(root).toMatchRenderedOutput(Final
);
});
- // @gate enableActivity
it('useDeferredValue can prerender the initial value inside a hidden tree', async () => {
function App({text}) {
const renderedText = useDeferredValue(text, `Preview [${text}]`);
@@ -805,7 +803,6 @@ describe('ReactDeferredValue', () => {
expect(root).toMatchRenderedOutput(B
);
});
- // @gate enableActivity
it(
'useDeferredValue skips the preview state when revealing a hidden tree ' +
'if the final value is referentially identical',
@@ -845,7 +842,6 @@ describe('ReactDeferredValue', () => {
},
);
- // @gate enableActivity
it(
'useDeferredValue does not skip the preview state when revealing a ' +
'hidden tree if the final value is different from the currently rendered one',
@@ -890,7 +886,6 @@ describe('ReactDeferredValue', () => {
},
);
- // @gate enableActivity
it(
'useDeferredValue does not show "previous" value when revealing a hidden ' +
'tree (no initial value)',
diff --git a/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js b/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
index 5b96ad1ee675..c761501839a0 100644
--- a/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
@@ -202,7 +202,6 @@ describe('ReactFragment', () => {
]);
});
- // @gate enableActivity
it('includes built-in for Activity', async () => {
ReactNoop.createRoot({
onCaughtError,
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
index b390bf4d1ee8..d82b6e31b4c8 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
@@ -3044,7 +3044,6 @@ describe('ReactHooksWithNoopRenderer', () => {
});
});
- // @gate enableActivity
it('warns when setState is called from offscreen deleted insertion effect cleanup', async () => {
function App(props) {
const [, setX] = useState(0);
diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
index 3afb0008ab58..0cb58d84a7ee 100644
--- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
@@ -116,6 +116,44 @@ describe('ReactLazy', () => {
expect(root).toMatchRenderedOutput('Hi again');
});
+ it('renders a lazy context provider', async () => {
+ const Context = React.createContext('default');
+ function ConsumerText() {
+ return ;
+ }
+ // Context.Provider === Context, so we can lazy-load the context itself
+ const LazyProvider = lazy(() => fakeImport(Context));
+
+ const root = ReactTestRenderer.create(
+ }>
+
+
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+
+ await waitForAll(['Loading...']);
+ expect(root).not.toMatchRenderedOutput('Hi');
+
+ await act(() => resolveFakeImport(Context));
+ assertLog(['Hi']);
+ expect(root).toMatchRenderedOutput('Hi');
+
+ // Should not suspend on update
+ root.update(
+ }>
+
+
+
+ ,
+ );
+ await waitForAll(['Hi again']);
+ expect(root).toMatchRenderedOutput('Hi again');
+ });
+
it('can resolve synchronously without suspending', async () => {
const LazyText = lazy(() => ({
then(cb) {
@@ -858,13 +896,20 @@ describe('ReactLazy', () => {
);
});
- it('throws with a useful error when wrapping Context with lazy()', async () => {
- const Context = React.createContext(null);
- const BadLazy = lazy(() => fakeImport(Context));
+ it('renders a lazy context provider without value prop', async () => {
+ // Context providers work when wrapped in lazy()
+ const Context = React.createContext('default');
+ const LazyProvider = lazy(() => fakeImport(Context));
+
+ function ConsumerText() {
+ return ;
+ }
const root = ReactTestRenderer.create(
}>
-
+
+
+
,
{
unstable_isConcurrent: true,
@@ -873,16 +918,9 @@ describe('ReactLazy', () => {
await waitForAll(['Loading...']);
- await resolveFakeImport(Context);
- root.update(
- }>
-
- ,
- );
- await waitForThrow(
- 'Element type is invalid. Received a promise that resolves to: Context. ' +
- 'Lazy element type must resolve to a class or function.',
- );
+ await act(() => resolveFakeImport(Context));
+ assertLog(['provided']);
+ expect(root).toMatchRenderedOutput('provided');
});
it('throws with a useful error when wrapping Context.Consumer with lazy()', async () => {
@@ -966,7 +1004,6 @@ describe('ReactLazy', () => {
);
});
- // @gate enableActivity
it('throws with a useful error when wrapping Activity with lazy()', async () => {
const BadLazy = lazy(() => fakeImport(React.Activity));
diff --git a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
index faaadb9cb617..5f5defb271cc 100644
--- a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js
@@ -523,4 +523,110 @@ describe('ReactPerformanceTracks', () => {
],
]);
});
+
+ // @gate __DEV__ && enableComponentPerformanceTrack
+ it('diffs HTML-like objects', async () => {
+ const App = function App({container}) {
+ Scheduler.unstable_advanceTime(10);
+ React.useEffect(() => {}, [container]);
+ };
+
+ class Window {}
+ const createOpaqueOriginWindow = () => {
+ return new Proxy(new Window(), {
+ get(target, prop) {
+ if (prop === Symbol.toStringTag) {
+ return target[Symbol.toStringTag];
+ }
+ // Some properties are allowed if JS itself is accessign those e.g.
+ // Symbol.toStringTag.
+ // Just make sure React isn't accessing arbitrary properties.
+ throw new Error(
+ `Failed to read named property '${String(prop)}' from Window`,
+ );
+ },
+ });
+ };
+
+ class OpaqueOriginHTMLIFrameElement {
+ constructor(textContent) {
+ this.textContent = textContent;
+ }
+ contentWindow = createOpaqueOriginWindow();
+ nodeType = 1;
+ [Symbol.toStringTag] = 'HTMLIFrameElement';
+ }
+
+ Scheduler.unstable_advanceTime(1);
+ await act(() => {
+ ReactNoop.render(
+ ,
+ );
+ });
+
+ expect(performanceMeasureCalls).toEqual([
+ [
+ 'Mount',
+ {
+ detail: {
+ devtools: {
+ color: 'warning',
+ properties: null,
+ tooltipText: 'Mount',
+ track: 'Components ⚛',
+ },
+ },
+ end: 11,
+ start: 1,
+ },
+ ],
+ ]);
+ performanceMeasureCalls.length = 0;
+
+ Scheduler.unstable_advanceTime(10);
+
+ await act(() => {
+ ReactNoop.render(
+ ,
+ );
+ });
+
+ expect(performanceMeasureCalls).toEqual([
+ [
+ 'App',
+ {
+ detail: {
+ devtools: {
+ color: 'primary-dark',
+ properties: [
+ ['Changed Props', ''],
+ ['- container', 'HTMLIFrameElement'],
+ ['- contentWindow', 'Window'],
+ ['- nodeType', '1'],
+ ['- textContent', '"foo"'],
+ ['+ container', 'HTMLIFrameElement'],
+ ['+ contentWindow', 'Window'],
+ ['+ nodeType', '1'],
+ ['+ textContent', '"bar"'],
+ [
+ ' contentWindow',
+ 'Referentially unequal but deeply equal objects. Consider memoization.',
+ ],
+ ],
+ tooltipText: 'App',
+ track: 'Components ⚛',
+ },
+ },
+ end: 31,
+ start: 21,
+ },
+ ],
+ ]);
+ });
});
diff --git a/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js b/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js
index 14dcea33ffe2..48d66a5e43d3 100644
--- a/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js
@@ -175,7 +175,6 @@ describe('ReactSiblingPrerendering', () => {
]);
});
- // @gate enableActivity
it("don't skip siblings when rendering inside a hidden tree", async () => {
function App() {
return (
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js
index 4c1003108550..6da915bd7112 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js
@@ -442,7 +442,7 @@ describe('ReactSuspenseyCommitPhase', () => {
);
});
- // @gate enableActivity && enableSuspenseyImages
+ // @gate enableSuspenseyImages
it("host instances don't suspend during prerendering, but do suspend when they are revealed", async () => {
function More() {
Scheduler.log('More');
diff --git a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
index d6e1bef955c3..cbadf5ec79e4 100644
--- a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+++ b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
@@ -305,7 +305,7 @@ describe('ReactFreshIntegration', () => {
}
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for class component in hidden subtree', async () => {
const code = `
import {Activity} from 'react';
@@ -335,7 +335,7 @@ describe('ReactFreshIntegration', () => {
await patch(code);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for hoistable resource in hidden subtree', async () => {
const code = `
import {Activity} from 'react';
@@ -357,7 +357,7 @@ describe('ReactFreshIntegration', () => {
await patch(code);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for host component in hidden subtree', async () => {
const code = `
import {Activity} from 'react';
@@ -379,7 +379,7 @@ describe('ReactFreshIntegration', () => {
await patch(code);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for Activity in hidden subtree', async () => {
const code = `
import {Activity} from 'react';
@@ -403,7 +403,7 @@ describe('ReactFreshIntegration', () => {
await patch(code);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for Scope in hidden subtree', async () => {
const code = `
import {
@@ -430,7 +430,7 @@ describe('ReactFreshIntegration', () => {
await patch(code);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for functional component in hidden subtree', async () => {
const code = `
import {Activity} from 'react';
@@ -458,7 +458,7 @@ describe('ReactFreshIntegration', () => {
await patch(code);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for ref forwarding component in hidden subtree', async () => {
const code = `
import {
@@ -489,7 +489,7 @@ describe('ReactFreshIntegration', () => {
await patch(code);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for simple memo component in hidden subtree', async () => {
const code = `
import {
@@ -520,7 +520,7 @@ describe('ReactFreshIntegration', () => {
await patch(code);
});
- // @gate __DEV__ && enableActivity
+ // @gate __DEV__
it('ignores ref for memo component in hidden subtree', async () => {
// A custom compare function means this won't use SimpleMemoComponent.
const code = `
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/ReactFlightTurbopackReferences.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
index 082a9c0ce583..50f83d31d8b1 100644
--- a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
+++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps = {
// $FlowFixMe[prop-missing]
return Object.prototype[Symbol.toStringTag];
case 'Provider':
- throw new Error(
- `Cannot render a Client Context Provider on the Server. ` +
- `Instead, you can export a Client Component wrapper ` +
- `that itself renders a Client Context Provider.`,
- );
+ // Context.Provider === Context in React, so return the same reference.
+ // This allows server components to render
+ // which will be serialized and executed on the client.
+ return receiver;
case 'then':
throw new Error(
`Cannot await or return from a thenable. ` +
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/ReactFlightUnbundledReferences.js b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js
index 07646e18ec40..b9f90b4d14b8 100644
--- a/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js
+++ b/packages/react-server-dom-unbundled/src/ReactFlightUnbundledReferences.js
@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps = {
// $FlowFixMe[prop-missing]
return Object.prototype[Symbol.toStringTag];
case 'Provider':
- throw new Error(
- `Cannot render a Client Context Provider on the Server. ` +
- `Instead, you can export a Client Component wrapper ` +
- `that itself renders a Client Context Provider.`,
- );
+ // Context.Provider === Context in React, so return the same reference.
+ // This allows server components to render
+ // which will be serialized and executed on the client.
+ return receiver;
case 'then':
throw new Error(
`Cannot await or return from a thenable. ` +
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/ReactFlightWebpackReferences.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
index 07646e18ec40..b9f90b4d14b8 100644
--- a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
+++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
@@ -182,11 +182,10 @@ const deepProxyHandlers: Proxy$traps = {
// $FlowFixMe[prop-missing]
return Object.prototype[Symbol.toStringTag];
case 'Provider':
- throw new Error(
- `Cannot render a Client Context Provider on the Server. ` +
- `Instead, you can export a Client Component wrapper ` +
- `that itself renders a Client Context Provider.`,
- );
+ // Context.Provider === Context in React, so return the same reference.
+ // This allows server components to render
+ // which will be serialized and executed on the client.
+ return receiver;
case 'then':
throw new Error(
`Cannot await or return from a thenable. ` +
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
index 728b8ac197d9..7070f39cea82 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
@@ -787,7 +787,7 @@ describe('ReactFlightDOM', () => {
;
});
- it('throws when accessing a Context.Provider below the client exports', () => {
+ it('does not throw when accessing a Context.Provider from client exports', () => {
const Context = React.createContext();
const ClientModule = clientExports({
Context,
@@ -795,11 +795,60 @@ describe('ReactFlightDOM', () => {
function dotting() {
return ClientModule.Context.Provider;
}
- expect(dotting).toThrowError(
- `Cannot render a Client Context Provider on the Server. ` +
- `Instead, you can export a Client Component wrapper ` +
- `that itself renders a Client Context Provider.`,
+ expect(dotting).not.toThrowError();
+ });
+
+ it('can render a client Context.Provider from a server component', async () => {
+ // Create a context in a client module
+ const TestContext = React.createContext('default');
+ const ClientModule = clientExports({
+ TestContext,
+ });
+
+ // Client component that reads context
+ function ClientConsumer() {
+ const value = React.useContext(TestContext);
+ return {value};
+ }
+ const {ClientConsumer: ClientConsumerRef} = clientExports({ClientConsumer});
+
+ function Print({response}) {
+ return use(response);
+ }
+
+ function App({response}) {
+ return (
+ Loading...}>
+
+
+ );
+ }
+
+ // Server component that provides context
+ function ServerApp() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const {writable, readable} = getTestStream();
+ const {pipe} = await serverAct(() =>
+ ReactServerDOMServer.renderToPipeableStream(, webpackMap),
);
+ pipe(writable);
+ const response = ReactServerDOMClient.createFromReadableStream(readable);
+
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.innerHTML).toBe('from-server
');
});
it('should progressively reveal server components', async () => {
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,
diff --git a/packages/react-server/src/ReactFlightServerTemporaryReferences.js b/packages/react-server/src/ReactFlightServerTemporaryReferences.js
index 581fdc70feb3..9195afa494c9 100644
--- a/packages/react-server/src/ReactFlightServerTemporaryReferences.js
+++ b/packages/react-server/src/ReactFlightServerTemporaryReferences.js
@@ -65,11 +65,10 @@ const proxyHandlers: Proxy$traps = {
// $FlowFixMe[prop-missing]
return Object.prototype[Symbol.toStringTag];
case 'Provider':
- throw new Error(
- `Cannot render a Client Context Provider on the Server. ` +
- `Instead, you can export a Client Component wrapper ` +
- `that itself renders a Client Context Provider.`,
- );
+ // Context.Provider === Context in React, so return the same reference.
+ // This allows server components to render
+ // which will be serialized and executed on the client.
+ return receiver;
case 'then':
// Allow returning a temporary reference from an async function
// Unlike regular Client References, a Promise would never have been serialized as
diff --git a/packages/shared/ReactPerformanceTrackProperties.js b/packages/shared/ReactPerformanceTrackProperties.js
index 3bca2fac60fd..29aba7282f04 100644
--- a/packages/shared/ReactPerformanceTrackProperties.js
+++ b/packages/shared/ReactPerformanceTrackProperties.js
@@ -82,6 +82,13 @@ export function addObjectToProperties(
}
}
+function readReactElementTypeof(value: Object): mixed {
+ // Prevents dotting into $$typeof in opaque origin windows.
+ return '$$typeof' in value && hasOwnProperty.call(value, '$$typeof')
+ ? value.$$typeof
+ : undefined;
+}
+
export function addValueToProperties(
propertyName: string,
value: mixed,
@@ -96,7 +103,7 @@ export function addValueToProperties(
desc = 'null';
break;
} else {
- if (value.$$typeof === REACT_ELEMENT_TYPE) {
+ if (readReactElementTypeof(value) === REACT_ELEMENT_TYPE) {
// JSX
const typeName = getComponentNameFromType(value.type) || '\u2026';
const key = value.key;
@@ -352,9 +359,10 @@ export function addObjectDiffToProperties(
typeof nextValue === 'object' &&
prevValue !== null &&
nextValue !== null &&
- prevValue.$$typeof === nextValue.$$typeof
+ readReactElementTypeof(prevValue) ===
+ readReactElementTypeof(nextValue)
) {
- if (nextValue.$$typeof === REACT_ELEMENT_TYPE) {
+ if (readReactElementTypeof(nextValue) === REACT_ELEMENT_TYPE) {
if (
prevValue.type === nextValue.type &&
prevValue.key === nextValue.key
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index dcee1c3b15e6..48481ef3e28b 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -39,7 +39,7 @@ export const disableLegacyContextForFunctionComponents: boolean = false;
export const disableLegacyMode: boolean = false;
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
export const disableTextareaChildren: boolean = false;
-export const enableAsyncDebugInfo: boolean = false;
+export const enableAsyncDebugInfo: boolean = true;
export const enableAsyncIterableChildren: boolean = false;
export const enableCPUSuspense: boolean = true;
export const enableCreateEventHandleAPI: boolean = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index f64a2165d0d4..93f2f1d478cb 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -24,7 +24,7 @@ export const disableLegacyContextForFunctionComponents: boolean = true;
export const disableLegacyMode: boolean = false;
export const disableSchedulerTimeoutInWorkLoop: boolean = false;
export const disableTextareaChildren: boolean = false;
-export const enableAsyncDebugInfo: boolean = false;
+export const enableAsyncDebugInfo: boolean = true;
export const enableAsyncIterableChildren: boolean = false;
export const enableCPUSuspense: boolean = false;
export const enableCreateEventHandleAPI: boolean = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 3db934f13def..01184784ea2e 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -10,7 +10,7 @@
import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
-export const enableAsyncDebugInfo: boolean = false;
+export const enableAsyncDebugInfo: boolean = true;
export const enableSchedulingProfiler: boolean = false;
export const enableProfilerTimer: boolean = __PROFILE__;
export const enableProfilerCommitHooks: boolean = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
index a80ca9eedda7..e9003ece4397 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
@@ -19,7 +19,7 @@ export const disableLegacyContextForFunctionComponents = false;
export const disableLegacyMode = false;
export const disableSchedulerTimeoutInWorkLoop = false;
export const disableTextareaChildren = false;
-export const enableAsyncDebugInfo = false;
+export const enableAsyncDebugInfo = true;
export const enableAsyncIterableChildren = false;
export const enableCPUSuspense = true;
export const enableCreateEventHandleAPI = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index d8a1e797d85a..675db5ba1c73 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -10,7 +10,7 @@
import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as ExportsType from './ReactFeatureFlags.test-renderer.www';
-export const enableAsyncDebugInfo: boolean = false;
+export const enableAsyncDebugInfo: boolean = true;
export const enableSchedulingProfiler: boolean = false;
export const enableProfilerTimer: boolean = __PROFILE__;
export const enableProfilerCommitHooks: boolean = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index 391c50e99561..03740acdb4cd 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -35,7 +35,6 @@ export const enableScrollEndPolyfill: boolean = __VARIANT__;
export const enableFragmentRefs: boolean = __VARIANT__;
export const enableFragmentRefsScrollIntoView: boolean = __VARIANT__;
export const enableFragmentRefsTextNodes: boolean = __VARIANT__;
-export const enableAsyncDebugInfo: boolean = __VARIANT__;
export const enableInternalInstanceMap: boolean = __VARIANT__;
export const enableTrustedTypesIntegration: boolean = __VARIANT__;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 5e206f489fec..3f8838221c7e 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -33,7 +33,6 @@ export const {
enableFragmentRefs,
enableFragmentRefsScrollIntoView,
enableFragmentRefsTextNodes,
- enableAsyncDebugInfo,
enableInternalInstanceMap,
} = dynamicFeatureFlags;
@@ -47,6 +46,7 @@ export const enableUpdaterTracking = __PROFILE__;
export const enableSuspenseAvoidThisFallback: boolean = true;
+export const enableAsyncDebugInfo: boolean = true;
export const enableCPUSuspense: boolean = true;
export const enableMoveBefore: boolean = false;
export const disableInputAttributeSyncing: boolean = false;