diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
index 95f3c479b9..2ccf1f16af 100644
--- a/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
+++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
@@ -2121,6 +2121,437 @@ Object {
}
`;
+exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 0 1`] = `
+Object {
+ "changeDescriptions": Map {
+ 3 => Object {
+ "context": null,
+ "didHooksChange": false,
+ "isFirstMount": true,
+ "props": null,
+ "state": null,
+ },
+ },
+ "duration": 0,
+ "fiberActualDurations": Map {
+ 1 => 0,
+ 2 => 0,
+ 3 => 0,
+ },
+ "fiberSelfDurations": Map {
+ 1 => 0,
+ 2 => 0,
+ 3 => 0,
+ },
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+}
+`;
+
+exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 1 1`] = `
+Object {
+ "changeDescriptions": Map {
+ 3 => Object {
+ "context": null,
+ "didHooksChange": false,
+ "isFirstMount": false,
+ "props": Array [
+ "count",
+ ],
+ "state": null,
+ },
+ },
+ "duration": 0,
+ "fiberActualDurations": Map {
+ 3 => 0,
+ 2 => 0,
+ 1 => 0,
+ },
+ "fiberSelfDurations": Map {
+ 3 => 0,
+ 2 => 0,
+ 1 => 0,
+ },
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+}
+`;
+
+exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 2 1`] = `
+Object {
+ "changeDescriptions": Map {
+ 3 => Object {
+ "context": null,
+ "didHooksChange": true,
+ "isFirstMount": false,
+ "props": Array [],
+ "state": null,
+ },
+ },
+ "duration": 0,
+ "fiberActualDurations": Map {
+ 3 => 0,
+ },
+ "fiberSelfDurations": Map {
+ 3 => 0,
+ },
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+}
+`;
+
+exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 3 1`] = `
+Object {
+ "changeDescriptions": Map {
+ 3 => Object {
+ "context": null,
+ "didHooksChange": true,
+ "isFirstMount": false,
+ "props": Array [],
+ "state": null,
+ },
+ },
+ "duration": 0,
+ "fiberActualDurations": Map {
+ 3 => 0,
+ },
+ "fiberSelfDurations": Map {
+ 3 => 0,
+ },
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+}
+`;
+
+exports[`ProfilingCache should properly detect changed hooks: CommitDetails commitIndex: 4 1`] = `
+Object {
+ "changeDescriptions": Map {
+ 3 => Object {
+ "context": null,
+ "didHooksChange": false,
+ "isFirstMount": false,
+ "props": Array [],
+ "state": null,
+ },
+ },
+ "duration": 0,
+ "fiberActualDurations": Map {
+ 3 => 0,
+ 2 => 0,
+ 1 => 0,
+ },
+ "fiberSelfDurations": Map {
+ 3 => 0,
+ 2 => 0,
+ 1 => 0,
+ },
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+}
+`;
+
+exports[`ProfilingCache should properly detect changed hooks: imported data 1`] = `
+Object {
+ "dataForRoots": Array [
+ Object {
+ "commitData": Array [
+ Object {
+ "changeDescriptions": Array [
+ Array [
+ 3,
+ Object {
+ "context": null,
+ "didHooksChange": false,
+ "isFirstMount": true,
+ "props": null,
+ "state": null,
+ },
+ ],
+ ],
+ "duration": 0,
+ "fiberActualDurations": Array [
+ Array [
+ 1,
+ 0,
+ ],
+ Array [
+ 2,
+ 0,
+ ],
+ Array [
+ 3,
+ 0,
+ ],
+ ],
+ "fiberSelfDurations": Array [
+ Array [
+ 1,
+ 0,
+ ],
+ Array [
+ 2,
+ 0,
+ ],
+ Array [
+ 3,
+ 0,
+ ],
+ ],
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+ },
+ Object {
+ "changeDescriptions": Array [
+ Array [
+ 3,
+ Object {
+ "context": null,
+ "didHooksChange": false,
+ "isFirstMount": false,
+ "props": Array [
+ "count",
+ ],
+ "state": null,
+ },
+ ],
+ ],
+ "duration": 0,
+ "fiberActualDurations": Array [
+ Array [
+ 3,
+ 0,
+ ],
+ Array [
+ 2,
+ 0,
+ ],
+ Array [
+ 1,
+ 0,
+ ],
+ ],
+ "fiberSelfDurations": Array [
+ Array [
+ 3,
+ 0,
+ ],
+ Array [
+ 2,
+ 0,
+ ],
+ Array [
+ 1,
+ 0,
+ ],
+ ],
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+ },
+ Object {
+ "changeDescriptions": Array [
+ Array [
+ 3,
+ Object {
+ "context": null,
+ "didHooksChange": true,
+ "isFirstMount": false,
+ "props": Array [],
+ "state": null,
+ },
+ ],
+ ],
+ "duration": 0,
+ "fiberActualDurations": Array [
+ Array [
+ 3,
+ 0,
+ ],
+ ],
+ "fiberSelfDurations": Array [
+ Array [
+ 3,
+ 0,
+ ],
+ ],
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+ },
+ Object {
+ "changeDescriptions": Array [
+ Array [
+ 3,
+ Object {
+ "context": null,
+ "didHooksChange": true,
+ "isFirstMount": false,
+ "props": Array [],
+ "state": null,
+ },
+ ],
+ ],
+ "duration": 0,
+ "fiberActualDurations": Array [
+ Array [
+ 3,
+ 0,
+ ],
+ ],
+ "fiberSelfDurations": Array [
+ Array [
+ 3,
+ 0,
+ ],
+ ],
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+ },
+ Object {
+ "changeDescriptions": Array [
+ Array [
+ 3,
+ Object {
+ "context": null,
+ "didHooksChange": false,
+ "isFirstMount": false,
+ "props": Array [],
+ "state": null,
+ },
+ ],
+ ],
+ "duration": 0,
+ "fiberActualDurations": Array [
+ Array [
+ 3,
+ 0,
+ ],
+ Array [
+ 2,
+ 0,
+ ],
+ Array [
+ 1,
+ 0,
+ ],
+ ],
+ "fiberSelfDurations": Array [
+ Array [
+ 3,
+ 0,
+ ],
+ Array [
+ 2,
+ 0,
+ ],
+ Array [
+ 1,
+ 0,
+ ],
+ ],
+ "interactionIDs": Array [],
+ "priorityLevel": "Immediate",
+ "timestamp": 0,
+ },
+ ],
+ "displayName": "Component",
+ "initialTreeBaseDurations": Array [],
+ "interactionCommits": Array [],
+ "interactions": Array [],
+ "operations": Array [
+ Array [
+ 1,
+ 1,
+ 27,
+ 16,
+ 67,
+ 111,
+ 110,
+ 116,
+ 101,
+ 120,
+ 116,
+ 46,
+ 80,
+ 114,
+ 111,
+ 118,
+ 105,
+ 100,
+ 101,
+ 114,
+ 9,
+ 67,
+ 111,
+ 109,
+ 112,
+ 111,
+ 110,
+ 101,
+ 110,
+ 116,
+ 1,
+ 1,
+ 11,
+ 1,
+ 1,
+ 1,
+ 2,
+ 2,
+ 1,
+ 0,
+ 1,
+ 0,
+ 4,
+ 2,
+ 0,
+ 1,
+ 3,
+ 5,
+ 2,
+ 0,
+ 2,
+ 0,
+ 4,
+ 3,
+ 0,
+ ],
+ Array [
+ 1,
+ 1,
+ 0,
+ ],
+ Array [
+ 1,
+ 1,
+ 0,
+ ],
+ Array [
+ 1,
+ 1,
+ 0,
+ ],
+ Array [
+ 1,
+ 1,
+ 0,
+ ],
+ ],
+ "rootID": 1,
+ "snapshots": Array [],
+ },
+ ],
+ "version": 4,
+}
+`;
+
exports[`ProfilingCache should record changed props/state/context/hooks: CommitDetails commitIndex: 0 1`] = `
Object {
"changeDescriptions": Map {
diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js
index f99be7bc3b..b26c8dbe68 100644
--- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js
@@ -308,6 +308,130 @@ describe('ProfilingCache', () => {
}
});
+ it('should properly detect changed hooks', () => {
+ const Context = React.createContext(0);
+
+ function reducer(state, action) {
+ switch (action.type) {
+ case 'invert':
+ return {value: !state.value};
+ default:
+ throw new Error();
+ }
+ }
+
+ let dispatch = null;
+ let setState = null;
+
+ const Component = ({count, string}) => {
+ // These hooks may change and initiate re-renders.
+ setState = React.useState('abc')[1];
+ dispatch = React.useReducer(reducer, {value: true})[1];
+
+ // This hook's return value may change between renders,
+ // but the hook itself isn't stateful.
+ React.useContext(Context);
+
+ // These hooks and their dependencies may not change between renders.
+ // We're using them to ensure that they don't trigger false positives.
+ React.useCallback(() => () => {}, [string]);
+ React.useMemo(() => string, [string]);
+
+ // These hooks never "change".
+ React.useEffect(() => {}, [string]);
+ React.useLayoutEffect(() => {}, [string]);
+
+ return null;
+ };
+
+ const container = document.createElement('div');
+
+ utils.act(() => store.profilerStore.startProfiling());
+ utils.act(() =>
+ ReactDOM.render(
+
+
+ ,
+ container,
+ ),
+ );
+
+ // Second render has no changed hooks, only changed props.
+ utils.act(() =>
+ ReactDOM.render(
+
+
+ ,
+ container,
+ ),
+ );
+
+ // Third render has a changed reducer hook
+ utils.act(() => dispatch({type: 'invert'}));
+
+ // Fourth render has a changed state hook
+ utils.act(() => setState('def'));
+
+ // Fifth render has a changed context value, but no changed hook.
+ // Technically, DevTools will miss this "context" change since it only tracks legacy context.
+ utils.act(() =>
+ ReactDOM.render(
+
+
+ ,
+ container,
+ ),
+ );
+
+ utils.act(() => store.profilerStore.stopProfiling());
+
+ const allCommitData = [];
+
+ function Validator({commitIndex, previousCommitDetails, rootID}) {
+ const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
+ if (previousCommitDetails != null) {
+ expect(commitData).toEqual(previousCommitDetails);
+ } else {
+ allCommitData.push(commitData);
+ expect(commitData).toMatchSnapshot(
+ `CommitDetails commitIndex: ${commitIndex}`,
+ );
+ }
+ return null;
+ }
+
+ const rootID = store.roots[0];
+
+ for (let commitIndex = 0; commitIndex < 5; commitIndex++) {
+ utils.act(() => {
+ TestRenderer.create(
+ ,
+ );
+ });
+ }
+
+ expect(allCommitData).toHaveLength(5);
+
+ // Export and re-import profile data and make sure it is retained.
+ utils.exportImportHelper(bridge, store);
+
+ for (let commitIndex = 0; commitIndex < 5; commitIndex++) {
+ utils.act(() => {
+ TestRenderer.create(
+ ,
+ );
+ });
+ }
+ });
+
it('should calculate a self duration based on actual children (not filtered children)', () => {
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];