diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js
index 4778daffcec2b..0d6d8d02a1989 100644
--- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js
@@ -1209,6 +1209,106 @@ describe('ProfilingCache', () => {
}
});
+ // @reactVersion >= 19.0
+ it('should detect context changes or lack of changes with conditional use()', () => {
+ const ContextA = React.createContext(0);
+ const ContextB = React.createContext(1);
+ let setState = null;
+
+ const Component = () => {
+ // These hooks may change and initiate re-renders.
+ let state;
+ [state, setState] = React.useState('abc');
+
+ let result = state;
+
+ if (state.includes('a')) {
+ result += React.use(ContextA);
+ }
+
+ result += React.use(ContextB);
+
+ return result;
+ };
+
+ utils.act(() =>
+ render(
+
+
+
+
+ ,
+ ),
+ );
+
+ utils.act(() => store.profilerStore.startProfiling());
+
+ // First render changes Context.
+ utils.act(() =>
+ render(
+
+
+
+
+ ,
+ ),
+ );
+
+ // Second render has no changed Context, only changed state.
+ utils.act(() => setState('def'));
+
+ utils.act(() => store.profilerStore.stopProfiling());
+
+ const rootID = store.roots[0];
+
+ const changeDescriptions = store.profilerStore
+ .getDataForRoot(rootID)
+ .commitData.map(commitData => commitData.changeDescriptions);
+ expect(changeDescriptions).toHaveLength(2);
+
+ // 1st render: Change to Context
+ expect(changeDescriptions[0]).toMatchInlineSnapshot(`
+ Map {
+ 4 => {
+ "context": true,
+ "didHooksChange": false,
+ "hooks": [],
+ "isFirstMount": false,
+ "props": [],
+ "state": null,
+ },
+ }
+ `);
+
+ // 2nd render: Change to State
+ expect(changeDescriptions[1]).toMatchInlineSnapshot(`
+ Map {
+ 4 => {
+ "context": false,
+ "didHooksChange": true,
+ "hooks": [
+ 0,
+ ],
+ "isFirstMount": false,
+ "props": [],
+ "state": null,
+ },
+ }
+ `);
+
+ expect(changeDescriptions).toHaveLength(2);
+
+ // Export and re-import profile data and make sure it is retained.
+ utils.exportImportHelper(bridge, store);
+
+ for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
+ const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
+ expect(commitData.changeDescriptions).toEqual(
+ changeDescriptions[commitIndex],
+ );
+ }
+ });
+
// @reactVersion >= 18.0
it('should calculate durations based on actual children (not filtered children)', () => {
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js
index 5e5167557f20c..b335457915265 100644
--- a/packages/react-devtools-shared/src/backend/fiber/renderer.js
+++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js
@@ -1764,6 +1764,14 @@ export function attach(
// For older versions, there's no good way to read the current context value after render has completed.
// This is because React maintains a stack of context values during render,
// but by the time DevTools is called, render has finished and the stack is empty.
+ if (prevContext.context !== nextContext.context) {
+ // If the order of context has changed, then the later context values might have
+ // changed too but the main reason it rerendered was earlier. Either an earlier
+ // context changed value but then we would have exited already. If we end up here
+ // it's because a state or props change caused the order of contexts used to change.
+ // So the main cause is not the contexts themselves.
+ return false;
+ }
if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) {
return true;
}