Devtools: Display actual pending state when inspecting `useTransition` (#28499)
Sebastian Silbermann committed
Mar 6, 2024 at 15:38 UTC
0066e0b68d90cc71a137f26b7c5bf6af73398fc2
3 files changed
+198
-4
packages/react-debug-tools/src/ReactDebugHooks.js
+6
-3
@@ -427,16 +427,19 @@ function useTransition(): [
427
// useTransition() composes multiple hooks internally.
428
// Advance the current hook index the same number of times
429
// so that subsequent hooks have the right memoized state.
430
- nextHook(); // State
430
+ const stateHook = nextHook();
431
nextHook(); // Callback
432
+
433
+ const isPending = stateHook !== null ? stateHook.memoizedState : false;
434
+
435
hookLog.push({
436
displayName: null,
437
primitive: 'Transition',
438
stackError: new Error(),
436
- value: undefined,
439
+ value: isPending,
440
debugInfo: null,
441
});
439
- return [false, callback => {}];
442
+ return [isPending, () => {}];
443
}
444
445
function useDeferredValue<T>(value: T, initialValue?: T): T {
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+163
-1
@@ -952,7 +952,7 @@ describe('ReactHooksInspectionIntegration', () => {
952
"isStateEditable": false,
953
"name": "Transition",
954
"subHooks": [],
955
- "value": undefined,
955
+ "value": false,
956
},
957
{
958
"debugInfo": null,
@@ -986,6 +986,168 @@ describe('ReactHooksInspectionIntegration', () => {
986
`);
987
});
988
989
+ it('should update isPending returned from useTransition', async () => {
990
+ const IndefiniteSuspender = React.lazy(() => new Promise(() => {}));
991
+ let startTransition;
992
+ function Foo(props) {
993
+ const [show, setShow] = React.useState(false);
994
+ const [isPending, _startTransition] = React.useTransition();
995
+ React.useMemo(() => 'hello', []);
996
+ React.useMemo(() => 'not used', []);
997
+
998
+ // Otherwise we capture the version from the react-debug-tools dispatcher.
999
+ if (startTransition === undefined) {
1000
+ startTransition = () => {
1001
+ _startTransition(() => {
1002
+ setShow(true);
1003
+ });
1004
+ };
1005
+ }
1006
+
1007
+ return (
1008
+ <React.Suspense fallback="Loading">
1009
+ {isPending ? 'Pending' : null}
1010
+ {show ? <IndefiniteSuspender /> : null}
1011
+ </React.Suspense>
1012
+ );
1013
+ }
1014
+ const renderer = await act(() => {
1015
+ return ReactTestRenderer.create(<Foo />, {isConcurrent: true});
1016
+ });
1017
+ expect(renderer).toMatchRenderedOutput(null);
1018
+ let childFiber = renderer.root.findByType(Foo)._currentFiber();
1019
+ let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
1020
+ expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
1021
+ [
1022
+ {
1023
+ "debugInfo": null,
1024
+ "hookSource": {
1025
+ "columnNumber": 0,
1026
+ "fileName": "**",
1027
+ "functionName": "Foo",
1028
+ "lineNumber": 0,
1029
+ },
1030
+ "id": 0,
1031
+ "isStateEditable": true,
1032
+ "name": "State",
1033
+ "subHooks": [],
1034
+ "value": false,
1035
+ },
1036
+ {
1037
+ "debugInfo": null,
1038
+ "hookSource": {
1039
+ "columnNumber": 0,
1040
+ "fileName": "**",
1041
+ "functionName": "Foo",
1042
+ "lineNumber": 0,
1043
+ },
1044
+ "id": 1,
1045
+ "isStateEditable": false,
1046
+ "name": "Transition",
1047
+ "subHooks": [],
1048
+ "value": false,
1049
+ },
1050
+ {
1051
+ "debugInfo": null,
1052
+ "hookSource": {
1053
+ "columnNumber": 0,
1054
+ "fileName": "**",
1055
+ "functionName": "Foo",
1056
+ "lineNumber": 0,
1057
+ },
1058
+ "id": 2,
1059
+ "isStateEditable": false,
1060
+ "name": "Memo",
1061
+ "subHooks": [],
1062
+ "value": "hello",
1063
+ },
1064
+ {
1065
+ "debugInfo": null,
1066
+ "hookSource": {
1067
+ "columnNumber": 0,
1068
+ "fileName": "**",
1069
+ "functionName": "Foo",
1070
+ "lineNumber": 0,
1071
+ },
1072
+ "id": 3,
1073
+ "isStateEditable": false,
1074
+ "name": "Memo",
1075
+ "subHooks": [],
1076
+ "value": "not used",
1077
+ },
1078
+ ]
1079
+ `);
1080
+
1081
+ await act(() => {
1082
+ startTransition();
1083
+ });
1084
+
1085
+ expect(renderer).toMatchRenderedOutput('Pending');
1086
+
1087
+ childFiber = renderer.root.findByType(Foo)._currentFiber();
1088
+ tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
1089
+ expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
1090
+ [
1091
+ {
1092
+ "debugInfo": null,
1093
+ "hookSource": {
1094
+ "columnNumber": 0,
1095
+ "fileName": "**",
1096
+ "functionName": "Foo",
1097
+ "lineNumber": 0,
1098
+ },
1099
+ "id": 0,
1100
+ "isStateEditable": true,
1101
+ "name": "State",
1102
+ "subHooks": [],
1103
+ "value": false,
1104
+ },
1105
+ {
1106
+ "debugInfo": null,
1107
+ "hookSource": {
1108
+ "columnNumber": 0,
1109
+ "fileName": "**",
1110
+ "functionName": "Foo",
1111
+ "lineNumber": 0,
1112
+ },
1113
+ "id": 1,
1114
+ "isStateEditable": false,
1115
+ "name": "Transition",
1116
+ "subHooks": [],
1117
+ "value": true,
1118
+ },
1119
+ {
1120
+ "debugInfo": null,
1121
+ "hookSource": {
1122
+ "columnNumber": 0,
1123
+ "fileName": "**",
1124
+ "functionName": "Foo",
1125
+ "lineNumber": 0,
1126
+ },
1127
+ "id": 2,
1128
+ "isStateEditable": false,
1129
+ "name": "Memo",
1130
+ "subHooks": [],
1131
+ "value": "hello",
1132
+ },
1133
+ {
1134
+ "debugInfo": null,
1135
+ "hookSource": {
1136
+ "columnNumber": 0,
1137
+ "fileName": "**",
1138
+ "functionName": "Foo",
1139
+ "lineNumber": 0,
1140
+ },
1141
+ "id": 3,
1142
+ "isStateEditable": false,
1143
+ "name": "Memo",
1144
+ "subHooks": [],
1145
+ "value": "not used",
1146
+ },
1147
+ ]
1148
+ `);
1149
+ });
1150
+
1151
it('should support useDeferredValue hook', () => {
1152
function Foo(props) {
1153
React.useDeferredValue('abc');
packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js
+29
@@ -120,6 +120,34 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref<any>) => any) {
120
}
121
const HocWithHooks = wrapWithHoc(FunctionWithHooks);
122
123
+const Suspendender = React.lazy(() => {
124
+ return new Promise<any>(resolve => {
125
+ setTimeout(() => {
126
+ resolve({
127
+ default: () => 'Finished!',
128
+ });
129
+ }, 3000);
130
+ });
131
+});
132
+function Transition() {
133
+ const [show, setShow] = React.useState(false);
134
+ const [isPending, startTransition] = React.useTransition();
135
+
136
+ return (
137
+ <div>
138
+ <React.Suspense fallback="Loading">
139
+ {isPending ? 'Pending' : null}
140
+ {show ? <Suspendender /> : null}
141
+ </React.Suspense>
142
+ {!show && (
143
+ <button onClick={() => startTransition(() => setShow(true))}>
144
+ Transition
145
+ </button>
146
+ )}
147
+ </div>
148
+ );
149
+}
150
+
151
function incrementWithDelay(previousState: number, formData: FormData) {
152
const incrementDelay = +formData.get('incrementDelay');
153
const shouldReject = formData.get('shouldReject');
@@ -183,6 +211,7 @@ export default function CustomHooks(): React.Node {
211
<MemoWithHooks />
212
<ForwardRefWithHooks />
213
<HocWithHooks />
214
+ <Transition />
215
<ErrorBoundary>
216
<Forms />
217
</ErrorBoundary>