@samitouri / QOS-React-2 / commits / 0066e0b68d

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>