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; }