Skip to content

Commit fa482ab

Browse files
committed
fix(react-devtools-custom): use direct custom hook values in hook paths
1 parent 4be53d2 commit fa482ab

3 files changed

Lines changed: 256 additions & 1 deletion

File tree

packages/react-devtools-custom/src/__tests__/profilingCache-test.js

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,230 @@ describe('ProfilingCache', () => {
785785
`);
786786
});
787787

788+
// @reactVersion >= 16.9
789+
it('should use only direct custom hook values when formatting hook paths', () => {
790+
function useConditionalStateHook(isRandom) {
791+
const x = isRandom ? 1 : 0;
792+
const [value, setValue] = React.useState(x);
793+
return [value, setValue];
794+
}
795+
796+
function useCustomHook(label) {
797+
React.useDebugValue(label);
798+
const [value, setValue] = React.useState(label);
799+
return [value, setValue];
800+
}
801+
802+
let setTopLevelState = null;
803+
let setConditionalState = null;
804+
let setNumberLabelState = null;
805+
let setStringLabelState = null;
806+
let setUndefinedLabelState = null;
807+
let setMissingLabelState = null;
808+
809+
const Component = () => {
810+
const [, _setTopLevelState] = React.useState(4);
811+
setTopLevelState = _setTopLevelState;
812+
813+
const [, _setConditionalState] = useConditionalStateHook(true);
814+
setConditionalState = _setConditionalState;
815+
816+
const [, _setNumberLabelState] = useCustomHook(10);
817+
setNumberLabelState = _setNumberLabelState;
818+
819+
const [, _setStringLabelState] = useCustomHook('10');
820+
setStringLabelState = _setStringLabelState;
821+
822+
const [, _setUndefinedLabelState] = useCustomHook(undefined);
823+
setUndefinedLabelState = _setUndefinedLabelState;
824+
825+
const [, _setMissingLabelState] = useCustomHook();
826+
setMissingLabelState = _setMissingLabelState;
827+
828+
return null;
829+
};
830+
831+
utils.act(() => render(<Component />));
832+
833+
const realSetTopLevelState = setTopLevelState;
834+
const realSetConditionalState = setConditionalState;
835+
const realSetNumberLabelState = setNumberLabelState;
836+
const realSetStringLabelState = setStringLabelState;
837+
const realSetUndefinedLabelState = setUndefinedLabelState;
838+
const realSetMissingLabelState = setMissingLabelState;
839+
840+
utils.act(() => startRecording());
841+
842+
utils.act(() => realSetTopLevelState(5));
843+
utils.act(() => realSetConditionalState(2));
844+
utils.act(() => realSetNumberLabelState(11));
845+
utils.act(() => realSetStringLabelState('11'));
846+
utils.act(() => realSetUndefinedLabelState('defined'));
847+
utils.act(() => realSetMissingLabelState('later'));
848+
849+
let recorded;
850+
utils.act(() => {
851+
recorded = endRecording();
852+
});
853+
854+
const changeDescriptions = recorded.map(commit => {
855+
const descriptions = toChangeDescriptionsByDisplayName(
856+
commit.filter(c => c.displayName === 'Component'),
857+
);
858+
return new Map(
859+
Array.from(descriptions.entries()).map(([displayName, entry]) => [
860+
displayName,
861+
{
862+
...entry,
863+
hooks:
864+
entry.hooks == null
865+
? entry.hooks
866+
: entry.hooks.map(({hookSource, ...hook}) => hook),
867+
},
868+
]),
869+
);
870+
});
871+
872+
expect(changeDescriptions).toHaveLength(6);
873+
874+
// 1st render: top-level useState(4)
875+
expect(changeDescriptions[0]).toMatchInlineSnapshot(`
876+
Map {
877+
"Component" => {
878+
"context": false,
879+
"didHooksChange": true,
880+
"hooks": [
881+
{
882+
"hookIndex": 0,
883+
"hookName": "State",
884+
"hookPath": [
885+
"State",
886+
],
887+
},
888+
],
889+
"isFirstMount": false,
890+
"props": [],
891+
"state": null,
892+
},
893+
}
894+
`);
895+
896+
// 2nd render: useConditionalStateHook with subhook state derived from x
897+
expect(changeDescriptions[1]).toMatchInlineSnapshot(`
898+
Map {
899+
"Component" => {
900+
"context": false,
901+
"didHooksChange": true,
902+
"hooks": [
903+
{
904+
"hookIndex": 1,
905+
"hookName": "State",
906+
"hookPath": [
907+
"ConditionalStateHook",
908+
"State",
909+
],
910+
},
911+
],
912+
"isFirstMount": false,
913+
"props": [],
914+
"state": null,
915+
},
916+
}
917+
`);
918+
919+
// 3rd render: useCustomHook(10)
920+
expect(changeDescriptions[2]).toMatchInlineSnapshot(`
921+
Map {
922+
"Component" => {
923+
"context": false,
924+
"didHooksChange": true,
925+
"hooks": [
926+
{
927+
"hookIndex": 2,
928+
"hookName": "State",
929+
"hookPath": [
930+
"CustomHook(10)",
931+
"State",
932+
],
933+
},
934+
],
935+
"isFirstMount": false,
936+
"props": [],
937+
"state": null,
938+
},
939+
}
940+
`);
941+
942+
// 4th render: useCustomHook("10")
943+
expect(changeDescriptions[3]).toMatchInlineSnapshot(`
944+
Map {
945+
"Component" => {
946+
"context": false,
947+
"didHooksChange": true,
948+
"hooks": [
949+
{
950+
"hookIndex": 3,
951+
"hookName": "State",
952+
"hookPath": [
953+
"CustomHook("10")",
954+
"State",
955+
],
956+
},
957+
],
958+
"isFirstMount": false,
959+
"props": [],
960+
"state": null,
961+
},
962+
}
963+
`);
964+
965+
// 5th render: useCustomHook(undefined)
966+
expect(changeDescriptions[4]).toMatchInlineSnapshot(`
967+
Map {
968+
"Component" => {
969+
"context": false,
970+
"didHooksChange": true,
971+
"hooks": [
972+
{
973+
"hookIndex": 4,
974+
"hookName": "State",
975+
"hookPath": [
976+
"CustomHook",
977+
"State",
978+
],
979+
},
980+
],
981+
"isFirstMount": false,
982+
"props": [],
983+
"state": null,
984+
},
985+
}
986+
`);
987+
988+
// 6th render: useCustomHook() without an argument
989+
expect(changeDescriptions[5]).toMatchInlineSnapshot(`
990+
Map {
991+
"Component" => {
992+
"context": false,
993+
"didHooksChange": true,
994+
"hooks": [
995+
{
996+
"hookIndex": 5,
997+
"hookName": "State",
998+
"hookPath": [
999+
"CustomHook",
1000+
"State",
1001+
],
1002+
},
1003+
],
1004+
"isFirstMount": false,
1005+
"props": [],
1006+
"state": null,
1007+
},
1008+
}
1009+
`);
1010+
});
1011+
7881012
// @reactVersion >= 16.9
7891013
it('should collect data for each rendered fiber', () => {
7901014
const Parent = ({count}) => {

packages/react-devtools-custom/src/onCommitFiber.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type {
2727
ResolvedHookChange,
2828
} from './collectFiberChanges';
2929
import collectFiberChanges from './collectFiberChanges';
30+
import getHookName from './util/getHookName';
3031

3132
let isRecording: boolean = false;
3233
type CommitRecord = {
@@ -75,9 +76,10 @@ function collectHooksByMemoizedStateIndex(
7576
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
7677
for (const hook of tree) {
7778
if (hook.subHooks.length > 0) {
79+
const hookName = getHookName(hook);
7880
memoizedStateIndex = collectHooksByMemoizedStateIndex(
7981
hook.subHooks,
80-
[...path, hook.name],
82+
[...path, hookName],
8183
hooksByMemoizedStateIndex,
8284
memoizedStateIndex,
8385
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
11+
12+
import {formatDataForPreview} from 'react-devtools-shared/src/utils';
13+
14+
function formatHookPathValue(value: mixed): string | null {
15+
if (value === undefined) {
16+
return null;
17+
}
18+
return formatDataForPreview(value, true);
19+
}
20+
21+
export default function getHookName(hook: HooksNode): string {
22+
const formattedHookPathValue = formatHookPathValue(hook.value);
23+
24+
if (formattedHookPathValue === null) {
25+
return hook.name;
26+
}
27+
28+
return `${hook.name}(${formattedHookPathValue})`;
29+
}

0 commit comments

Comments
 (0)