@samitouri / QOS-React / commits / 7a32d718b9

[Debug Tools] Introspect Promises in use() (#28297)

Alternative to #28295. Instead of stashing all of the Usables eagerly, we can extract them by replaying the render when we need them like we do with any other hook. We already had an implementation of `use()` but it wasn't quite complete. These can also include further DebugInfo on them such as what Server Component rendered the Promise or async debug info. This is nice just to see which use() calls were made in the side-panel but it can also be used to gather everything that might have suspended. Together with https://github.com/facebook/react/pull/28286 we cover the case when a Promise was used a child and if it was unwrapped with use(). Notably we don't cover a Promise that was thrown (although we do support that in a Server Component which maybe we shouldn't). Throwing a Promise isn't officially supported though and that use case should move to the use() Hook. The pattern of conditionally suspending based on cache also isn't really supported with the use() pattern. You should always call use() if you previously called use() with the same input. This also ensures that we can track what might have suspended rather than what actually did. One limitation of this strategy is that it's hard to find all the places something might suspend in a tree without rerendering all the fibers again. So we might need to still add something to the tree to indicate which Fibers may have further debug info / thenables.

Sebastian Markbåge committed Feb 12, 2024 at 17:54 UTC 7a32d718b9ea0eb9ea86e9d21d56a5af6c4ce9ed
4 files changed +347 -16
packages/react-debug-tools/src/ReactDebugHooks.js
+120 -11
@@ -13,6 +13,8 @@ import type {
13 ReactProviderType,
14 StartTransitionOptions,
15 Usable,
16 + Thenable,
17 + ReactDebugInfo,
18 } from 'shared/ReactTypes';
19 import type {
20 Fiber,
@@ -41,6 +43,7 @@ type HookLogEntry = {
43 primitive: string,
44 stackError: Error,
45 value: mixed,
46 + debugInfo: ReactDebugInfo | null,
47 };
48
49 let hookLog: Array<HookLogEntry> = [];
@@ -93,6 +96,27 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
96 // This type check is for Flow only.
97 Dispatcher.useFormState((s: mixed, p: mixed) => s, null);
98 }
99 + if (typeof Dispatcher.use === 'function') {
100 + // This type check is for Flow only.
101 + Dispatcher.use(
102 + ({
103 + $$typeof: REACT_CONTEXT_TYPE,
104 + _currentValue: null,
105 + }: any),
106 + );
107 + Dispatcher.use({
108 + then() {},
109 + status: 'fulfilled',
110 + value: null,
111 + });
112 + try {
113 + Dispatcher.use(
114 + ({
115 + then() {},
116 + }: any),
117 + );
118 + } catch (x) {}
119 + }
120 } finally {
121 readHookLog = hookLog;
122 hookLog = [];
@@ -122,22 +146,57 @@ function readContext<T>(context: ReactContext<T>): T {
146 return context._currentValue;
147 }
148
149 +const SuspenseException: mixed = new Error(
150 + "Suspense Exception: This is not a real error! It's an implementation " +
151 + 'detail of `use` to interrupt the current render. You must either ' +
152 + 'rethrow it immediately, or move the `use` call outside of the ' +
153 + '`try/catch` block. Capturing without rethrowing will lead to ' +
154 + 'unexpected behavior.\n\n' +
155 + 'To handle async errors, wrap your component in an error boundary, or ' +
156 + "call the promise's `.catch` method and pass the result to `use`",
157 +);
158 +
159 function use<T>(usable: Usable<T>): T {
160 if (usable !== null && typeof usable === 'object') {
161 // $FlowFixMe[method-unbinding]
162 if (typeof usable.then === 'function') {
129 - // TODO: What should this do if it receives an unresolved promise?
130 - throw new Error(
131 - 'Support for `use(Promise)` not yet implemented in react-debug-tools.',
132 - );
163 + const thenable: Thenable<any> = (usable: any);
164 + switch (thenable.status) {
165 + case 'fulfilled': {
166 + const fulfilledValue: T = thenable.value;
167 + hookLog.push({
168 + primitive: 'Promise',
169 + stackError: new Error(),
170 + value: fulfilledValue,
171 + debugInfo:
172 + thenable._debugInfo === undefined ? null : thenable._debugInfo,
173 + });
174 + return fulfilledValue;
175 + }
176 + case 'rejected': {
177 + const rejectedError = thenable.reason;
178 + throw rejectedError;
179 + }
180 + }
181 + // If this was an uncached Promise we have to abandon this attempt
182 + // but we can still emit anything up until this point.
183 + hookLog.push({
184 + primitive: 'Unresolved',
185 + stackError: new Error(),
186 + value: thenable,
187 + debugInfo:
188 + thenable._debugInfo === undefined ? null : thenable._debugInfo,
189 + });
190 + throw SuspenseException;
191 } else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
192 const context: ReactContext<T> = (usable: any);
193 const value = readContext(context);
194
195 hookLog.push({
138 - primitive: 'Use',
196 + primitive: 'Context (use)',
197 stackError: new Error(),
198 value,
199 + debugInfo: null,
200 });
201
202 return value;
@@ -153,6 +212,7 @@ function useContext<T>(context: ReactContext<T>): T {
212 primitive: 'Context',
213 stackError: new Error(),
214 value: context._currentValue,
215 + debugInfo: null,
216 });
217 return context._currentValue;
218 }
@@ -168,7 +228,12 @@ function useState<S>(
228 ? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
229 initialState()
230 : initialState;
171 - hookLog.push({primitive: 'State', stackError: new Error(), value: state});
231 + hookLog.push({
232 + primitive: 'State',
233 + stackError: new Error(),
234 + value: state,
235 + debugInfo: null,
236 + });
237 return [state, (action: BasicStateAction<S>) => {}];
238 }
239
@@ -188,6 +253,7 @@ function useReducer<S, I, A>(
253 primitive: 'Reducer',
254 stackError: new Error(),
255 value: state,
256 + debugInfo: null,
257 });
258 return [state, (action: A) => {}];
259 }
@@ -199,6 +265,7 @@ function useRef<T>(initialValue: T): {current: T} {
265 primitive: 'Ref',
266 stackError: new Error(),
267 value: ref.current,
268 + debugInfo: null,
269 });
270 return ref;
271 }
@@ -209,6 +276,7 @@ function useCacheRefresh(): () => void {
276 primitive: 'CacheRefresh',
277 stackError: new Error(),
278 value: hook !== null ? hook.memoizedState : function refresh() {},
279 + debugInfo: null,
280 });
281 return () => {};
282 }
@@ -222,6 +290,7 @@ function useLayoutEffect(
290 primitive: 'LayoutEffect',
291 stackError: new Error(),
292 value: create,
293 + debugInfo: null,
294 });
295 }
296
@@ -234,6 +303,7 @@ function useInsertionEffect(
303 primitive: 'InsertionEffect',
304 stackError: new Error(),
305 value: create,
306 + debugInfo: null,
307 });
308 }
309
@@ -242,7 +312,12 @@ function useEffect(
312 inputs: Array<mixed> | void | null,
313 ): void {
314 nextHook();
245 - hookLog.push({primitive: 'Effect', stackError: new Error(), value: create});
315 + hookLog.push({
316 + primitive: 'Effect',
317 + stackError: new Error(),
318 + value: create,
319 + debugInfo: null,
320 + });
321 }
322
323 function useImperativeHandle<T>(
@@ -263,6 +338,7 @@ function useImperativeHandle<T>(
338 primitive: 'ImperativeHandle',
339 stackError: new Error(),
340 value: instance,
341 + debugInfo: null,
342 });
343 }
344
@@ -271,6 +347,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
347 primitive: 'DebugValue',
348 stackError: new Error(),
349 value: typeof formatterFn === 'function' ? formatterFn(value) : value,
350 + debugInfo: null,
351 });
352 }
353
@@ -280,6 +357,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
357 primitive: 'Callback',
358 stackError: new Error(),
359 value: hook !== null ? hook.memoizedState[0] : callback,
360 + debugInfo: null,
361 });
362 return callback;
363 }
@@ -290,7 +368,12 @@ function useMemo<T>(
368 ): T {
369 const hook = nextHook();
370 const value = hook !== null ? hook.memoizedState[0] : nextCreate();
293 - hookLog.push({primitive: 'Memo', stackError: new Error(), value});
371 + hookLog.push({
372 + primitive: 'Memo',
373 + stackError: new Error(),
374 + value,
375 + debugInfo: null,
376 + });
377 return value;
378 }
379
@@ -309,6 +392,7 @@ function useSyncExternalStore<T>(
392 primitive: 'SyncExternalStore',
393 stackError: new Error(),
394 value,
395 + debugInfo: null,
396 });
397 return value;
398 }
@@ -326,6 +410,7 @@ function useTransition(): [
410 primitive: 'Transition',
411 stackError: new Error(),
412 value: undefined,
413 + debugInfo: null,
414 });
415 return [false, callback => {}];
416 }
@@ -336,6 +421,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
421 primitive: 'DeferredValue',
422 stackError: new Error(),
423 value: hook !== null ? hook.memoizedState : value,
424 + debugInfo: null,
425 });
426 return value;
427 }
@@ -347,6 +433,7 @@ function useId(): string {
433 primitive: 'Id',
434 stackError: new Error(),
435 value: id,
436 + debugInfo: null,
437 });
438 return id;
439 }
@@ -395,6 +482,7 @@ function useOptimistic<S, A>(
482 primitive: 'Optimistic',
483 stackError: new Error(),
484 value: state,
485 + debugInfo: null,
486 });
487 return [state, (action: A) => {}];
488 }
@@ -416,6 +504,7 @@ function useFormState<S, P>(
504 primitive: 'FormState',
505 stackError: new Error(),
506 value: state,
507 + debugInfo: null,
508 });
509 return [state, (payload: P) => {}];
510 }
@@ -480,6 +569,7 @@ export type HooksNode = {
569 name: string,
570 value: mixed,
571 subHooks: Array<HooksNode>,
572 + debugInfo: null | ReactDebugInfo,
573 hookSource?: HookSource,
574 };
575 export type HooksTree = Array<HooksNode>;
@@ -546,6 +636,15 @@ function isReactWrapper(functionName: any, primitiveName: string) {
636 if (!functionName) {
637 return false;
638 }
639 + switch (primitiveName) {
640 + case 'Context':
641 + case 'Context (use)':
642 + case 'Promise':
643 + case 'Unresolved':
644 + if (functionName.endsWith('use')) {
645 + return true;
646 + }
647 + }
648 const expectedPrimitiveName = 'use' + primitiveName;
649 if (functionName.length < expectedPrimitiveName.length) {
650 return false;
@@ -661,6 +760,7 @@ function buildTree(
760 name: parseCustomHookName(stack[j - 1].functionName),
761 value: undefined,
762 subHooks: children,
763 + debugInfo: null,
764 };
765
766 if (includeHooksSource) {
@@ -678,25 +778,29 @@ function buildTree(
778 }
779 prevStack = stack;
780 }
681 - const {primitive} = hook;
781 + const {primitive, debugInfo} = hook;
782
783 // For now, the "id" of stateful hooks is just the stateful hook index.
784 // Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
785 const id =
786 primitive === 'Context' ||
787 + primitive === 'Context (use)' ||
788 primitive === 'DebugValue' ||
688 - primitive === 'Use'
789 + primitive === 'Promise' ||
790 + primitive === 'Unresolved'
791 ? null
792 : nativeHookID++;
793
794 // For the time being, only State and Reducer hooks support runtime overrides.
795 const isStateEditable = primitive === 'Reducer' || primitive === 'State';
796 + const name = primitive === 'Context (use)' ? 'Context' : primitive;
797 const levelChild: HooksNode = {
798 id,
799 isStateEditable,
697 - name: primitive,
800 + name: name,
801 value: hook.value,
802 subHooks: [],
803 + debugInfo: debugInfo,
804 };
805
806 if (includeHooksSource) {
@@ -762,6 +866,11 @@ function processDebugValues(
866
867 function handleRenderFunctionError(error: any): void {
868 // original error might be any type.
869 + if (error === SuspenseException) {
870 + // An uncached Promise was used. We can't synchronously resolve the rest of
871 + // the Hooks but we can at least show what ever we got so far.
872 + return;
873 + }
874 if (
875 error instanceof Error &&
876 error.name === 'ReactDebugToolsUnsupportedHookError'
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
+102
@@ -31,6 +31,7 @@ describe('ReactHooksInspection', () => {
31 id: 0,
32 name: 'State',
33 value: 'hello world',
34 + debugInfo: null,
35 subHooks: [],
36 },
37 ]);
@@ -53,12 +54,14 @@ describe('ReactHooksInspection', () => {
54 id: null,
55 name: 'Custom',
56 value: __DEV__ ? 'custom hook label' : undefined,
57 + debugInfo: null,
58 subHooks: [
59 {
60 isStateEditable: true,
61 id: 0,
62 name: 'State',
63 value: 'hello world',
64 + debugInfo: null,
65 subHooks: [],
66 },
67 ],
@@ -89,11 +92,13 @@ describe('ReactHooksInspection', () => {
92 id: null,
93 name: 'Custom',
94 value: undefined,
95 + debugInfo: null,
96 subHooks: [
97 {
98 isStateEditable: true,
99 id: 0,
100 name: 'State',
101 + debugInfo: null,
102 subHooks: [],
103 value: 'hello',
104 },
@@ -101,6 +106,7 @@ describe('ReactHooksInspection', () => {
106 isStateEditable: false,
107 id: 1,
108 name: 'Effect',
109 + debugInfo: null,
110 subHooks: [],
111 value: effect,
112 },
@@ -111,12 +117,14 @@ describe('ReactHooksInspection', () => {
117 id: null,
118 name: 'Custom',
119 value: undefined,
120 + debugInfo: null,
121 subHooks: [
122 {
123 isStateEditable: true,
124 id: 2,
125 name: 'State',
126 value: 'world',
127 + debugInfo: null,
128 subHooks: [],
129 },
130 {
@@ -124,6 +132,7 @@ describe('ReactHooksInspection', () => {
132 id: 3,
133 name: 'Effect',
134 value: effect,
135 + debugInfo: null,
136 subHooks: [],
137 },
138 ],
@@ -164,18 +173,21 @@ describe('ReactHooksInspection', () => {
173 id: null,
174 name: 'Bar',
175 value: undefined,
176 + debugInfo: null,
177 subHooks: [
178 {
179 isStateEditable: false,
180 id: null,
181 name: 'Custom',
182 value: undefined,
183 + debugInfo: null,
184 subHooks: [
185 {
186 isStateEditable: true,
187 id: 0,
188 name: 'Reducer',
189 value: 'hello',
190 + debugInfo: null,
191 subHooks: [],
192 },
193 {
@@ -183,6 +195,7 @@ describe('ReactHooksInspection', () => {
195 id: 1,
196 name: 'Effect',
197 value: effect,
198 + debugInfo: null,
199 subHooks: [],
200 },
201 ],
@@ -192,6 +205,7 @@ describe('ReactHooksInspection', () => {
205 id: 2,
206 name: 'LayoutEffect',
207 value: effect,
208 + debugInfo: null,
209 subHooks: [],
210 },
211 ],
@@ -201,23 +215,27 @@ describe('ReactHooksInspection', () => {
215 id: null,
216 name: 'Baz',
217 value: undefined,
218 + debugInfo: null,
219 subHooks: [
220 {
221 isStateEditable: false,
222 id: 3,
223 name: 'LayoutEffect',
224 value: effect,
225 + debugInfo: null,
226 subHooks: [],
227 },
228 {
229 isStateEditable: false,
230 id: null,
231 name: 'Custom',
232 + debugInfo: null,
233 subHooks: [
234 {
235 isStateEditable: true,
236 id: 4,
237 name: 'Reducer',
238 + debugInfo: null,
239 subHooks: [],
240 value: 'world',
241 },
@@ -225,6 +243,7 @@ describe('ReactHooksInspection', () => {
243 isStateEditable: false,
244 id: 5,
245 name: 'Effect',
246 + debugInfo: null,
247 subHooks: [],
248 value: effect,
249 },
@@ -249,6 +268,7 @@ describe('ReactHooksInspection', () => {
268 id: null,
269 name: 'Context',
270 value: 'default',
271 + debugInfo: null,
272 subHooks: [],
273 },
274 ]);
@@ -287,6 +307,86 @@ describe('ReactHooksInspection', () => {
307 expect(setterCalls[1]).toBe(initial);
308 });
309
310 + it('should inspect use() calls for Promise and Context', async () => {
311 + const MyContext = React.createContext('hi');
312 + const promise = Promise.resolve('world');
313 + await promise;
314 + promise.status = 'fulfilled';
315 + promise.value = 'world';
316 + promise._debugInfo = [{name: 'Hello'}];
317 +
318 + function useCustom() {
319 + const value = React.use(promise);
320 + const [state] = React.useState(value);
321 + return state;
322 + }
323 + function Foo(props) {
324 + const value1 = React.use(MyContext);
325 + const value2 = useCustom();
326 + return (
327 + <div>
328 + {value1} {value2}
329 + </div>
330 + );
331 + }
332 + const tree = ReactDebugTools.inspectHooks(Foo, {});
333 + expect(tree).toEqual([
334 + {
335 + isStateEditable: false,
336 + id: null,
337 + name: 'Context',
338 + value: 'hi',
339 + debugInfo: null,
340 + subHooks: [],
341 + },
342 + {
343 + isStateEditable: false,
344 + id: null,
345 + name: 'Custom',
346 + value: undefined,
347 + debugInfo: null,
348 + subHooks: [
349 + {
350 + isStateEditable: false,
351 + id: null,
352 + name: 'Promise',
353 + value: 'world',
354 + debugInfo: [{name: 'Hello'}],
355 + subHooks: [],
356 + },
357 + {
358 + isStateEditable: true,
359 + id: 0,
360 + name: 'State',
361 + value: 'world',
362 + debugInfo: null,
363 + subHooks: [],
364 + },
365 + ],
366 + },
367 + ]);
368 + });
369 +
370 + it('should inspect use() calls for unresolved Promise', () => {
371 + const promise = Promise.resolve('hi');
372 +
373 + function Foo(props) {
374 + const value = React.use(promise);
375 + return <div>{value}</div>;
376 + }
377 + const tree = ReactDebugTools.inspectHooks(Foo, {});
378 + expect(tree).toEqual([
379 + {
380 + isStateEditable: false,
381 + id: null,
382 + name: 'Unresolved',
383 + value: promise,
384 + debugInfo: null,
385 + subHooks: [],
386 + },
387 + ]);
388 + });
389 +
390 describe('useDebugValue', () => {
391 it('should be ignored when called outside of a custom hook', () => {
392 function Foo(props) {
@@ -313,11 +413,13 @@ describe('ReactHooksInspection', () => {
413 id: null,
414 name: 'Custom',
415 value: __DEV__ ? 'bar:123' : undefined,
416 + debugInfo: null,
417 subHooks: [
418 {
419 isStateEditable: true,
420 id: 0,
421 name: 'State',
422 + debugInfo: null,
423 subHooks: [],
424 value: 0,
425 },
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+114 -5
@@ -48,6 +48,7 @@ describe('ReactHooksInspectionIntegration', () => {
48 id: 0,
49 name: 'State',
50 value: 'hello',
51 + debugInfo: null,
52 subHooks: [],
53 },
54 {
@@ -55,6 +56,7 @@ describe('ReactHooksInspectionIntegration', () => {
56 id: 1,
57 name: 'State',
58 value: 'world',
59 + debugInfo: null,
60 subHooks: [],
61 },
62 ]);
@@ -73,6 +75,7 @@ describe('ReactHooksInspectionIntegration', () => {
75 id: 0,
76 name: 'State',
77 value: 'Hi',
78 + debugInfo: null,
79 subHooks: [],
80 },
81 {
@@ -80,6 +83,7 @@ describe('ReactHooksInspectionIntegration', () => {
83 id: 1,
84 name: 'State',
85 value: 'world',
86 + debugInfo: null,
87 subHooks: [],
88 },
89 ]);
@@ -95,6 +99,7 @@ describe('ReactHooksInspectionIntegration', () => {
99 id: 0,
100 name: 'State',
101 value: 'Hi',
102 + debugInfo: null,
103 subHooks: [],
104 },
105 {
@@ -102,6 +107,7 @@ describe('ReactHooksInspectionIntegration', () => {
107 id: 1,
108 name: 'State',
109 value: 'world!',
110 + debugInfo: null,
111 subHooks: [],
112 },
113 ]);
@@ -157,6 +163,7 @@ describe('ReactHooksInspectionIntegration', () => {
163 id: 0,
164 name: 'State',
165 value: 'a',
166 + debugInfo: null,
167 subHooks: [],
168 },
169 {
@@ -164,14 +171,23 @@ describe('ReactHooksInspectionIntegration', () => {
171 id: 1,
172 name: 'Reducer',
173 value: 'b',
174 + debugInfo: null,
175 + subHooks: [],
176 + },
177 + {
178 + isStateEditable: false,
179 + id: 2,
180 + name: 'Ref',
181 + value: 'c',
182 + debugInfo: null,
183 subHooks: [],
184 },
169 - {isStateEditable: false, id: 2, name: 'Ref', value: 'c', subHooks: []},
185 {
186 isStateEditable: false,
187 id: 3,
188 name: 'LayoutEffect',
189 value: effect,
190 + debugInfo: null,
191 subHooks: [],
192 },
193 {
@@ -179,6 +195,7 @@ describe('ReactHooksInspectionIntegration', () => {
195 id: 4,
196 name: 'Effect',
197 value: effect,
198 + debugInfo: null,
199 subHooks: [],
200 },
201 {
@@ -186,6 +203,7 @@ describe('ReactHooksInspectionIntegration', () => {
203 id: 5,
204 name: 'ImperativeHandle',
205 value: outsideRef.current,
206 + debugInfo: null,
207 subHooks: [],
208 },
209 {
@@ -193,6 +211,7 @@ describe('ReactHooksInspectionIntegration', () => {
211 id: 6,
212 name: 'Memo',
213 value: 'ab',
214 + debugInfo: null,
215 subHooks: [],
216 },
217 {
@@ -200,6 +219,7 @@ describe('ReactHooksInspectionIntegration', () => {
219 id: 7,
220 name: 'Callback',
221 value: updateStates,
222 + debugInfo: null,
223 subHooks: [],
224 },
225 ]);
@@ -217,6 +237,7 @@ describe('ReactHooksInspectionIntegration', () => {
237 id: 0,
238 name: 'State',
239 value: 'A',
240 + debugInfo: null,
241 subHooks: [],
242 },
243 {
@@ -224,14 +245,23 @@ describe('ReactHooksInspectionIntegration', () => {
245 id: 1,
246 name: 'Reducer',
247 value: 'B',
248 + debugInfo: null,
249 + subHooks: [],
250 + },
251 + {
252 + isStateEditable: false,
253 + id: 2,
254 + name: 'Ref',
255 + value: 'C',
256 + debugInfo: null,
257 subHooks: [],
258 },
229 - {isStateEditable: false, id: 2, name: 'Ref', value: 'C', subHooks: []},
259 {
260 isStateEditable: false,
261 id: 3,
262 name: 'LayoutEffect',
263 value: effect,
264 + debugInfo: null,
265 subHooks: [],
266 },
267 {
@@ -239,6 +269,7 @@ describe('ReactHooksInspectionIntegration', () => {
269 id: 4,
270 name: 'Effect',
271 value: effect,
272 + debugInfo: null,
273 subHooks: [],
274 },
275 {
@@ -246,6 +277,7 @@ describe('ReactHooksInspectionIntegration', () => {
277 id: 5,
278 name: 'ImperativeHandle',
279 value: outsideRef.current,
280 + debugInfo: null,
281 subHooks: [],
282 },
283 {
@@ -253,6 +285,7 @@ describe('ReactHooksInspectionIntegration', () => {
285 id: 6,
286 name: 'Memo',
287 value: 'Ab',
288 + debugInfo: null,
289 subHooks: [],
290 },
291 {
@@ -260,6 +293,7 @@ describe('ReactHooksInspectionIntegration', () => {
293 id: 7,
294 name: 'Callback',
295 value: updateStates,
296 + debugInfo: null,
297 subHooks: [],
298 },
299 ]);
@@ -317,6 +351,7 @@ describe('ReactHooksInspectionIntegration', () => {
351 id: 0,
352 name: 'State',
353 value: 'a',
354 + debugInfo: null,
355 subHooks: [],
356 },
357 {
@@ -324,14 +359,23 @@ describe('ReactHooksInspectionIntegration', () => {
359 id: 1,
360 name: 'Reducer',
361 value: 'b',
362 + debugInfo: null,
363 + subHooks: [],
364 + },
365 + {
366 + isStateEditable: false,
367 + id: 2,
368 + name: 'Ref',
369 + value: 'c',
370 + debugInfo: null,
371 subHooks: [],
372 },
329 - {isStateEditable: false, id: 2, name: 'Ref', value: 'c', subHooks: []},
373 {
374 isStateEditable: false,
375 id: 3,
376 name: 'InsertionEffect',
377 value: effect,
378 + debugInfo: null,
379 subHooks: [],
380 },
381 {
@@ -339,6 +383,7 @@ describe('ReactHooksInspectionIntegration', () => {
383 id: 4,
384 name: 'LayoutEffect',
385 value: effect,
386 + debugInfo: null,
387 subHooks: [],
388 },
389 {
@@ -346,6 +391,7 @@ describe('ReactHooksInspectionIntegration', () => {
391 id: 5,
392 name: 'Effect',
393 value: effect,
394 + debugInfo: null,
395 subHooks: [],
396 },
397 {
@@ -353,6 +399,7 @@ describe('ReactHooksInspectionIntegration', () => {
399 id: 6,
400 name: 'ImperativeHandle',
401 value: outsideRef.current,
402 + debugInfo: null,
403 subHooks: [],
404 },
405 {
@@ -360,6 +407,7 @@ describe('ReactHooksInspectionIntegration', () => {
407 id: 7,
408 name: 'Memo',
409 value: 'ab',
410 + debugInfo: null,
411 subHooks: [],
412 },
413 {
@@ -367,6 +415,7 @@ describe('ReactHooksInspectionIntegration', () => {
415 id: 8,
416 name: 'Callback',
417 value: updateStates,
418 + debugInfo: null,
419 subHooks: [],
420 },
421 ]);
@@ -384,6 +433,7 @@ describe('ReactHooksInspectionIntegration', () => {
433 id: 0,
434 name: 'State',
435 value: 'A',
436 + debugInfo: null,
437 subHooks: [],
438 },
439 {
@@ -391,14 +441,23 @@ describe('ReactHooksInspectionIntegration', () => {
441 id: 1,
442 name: 'Reducer',
443 value: 'B',
444 + debugInfo: null,
445 + subHooks: [],
446 + },
447 + {
448 + isStateEditable: false,
449 + id: 2,
450 + name: 'Ref',
451 + value: 'C',
452 + debugInfo: null,
453 subHooks: [],
454 },
396 - {isStateEditable: false, id: 2, name: 'Ref', value: 'C', subHooks: []},
455 {
456 isStateEditable: false,
457 id: 3,
458 name: 'InsertionEffect',
459 value: effect,
460 + debugInfo: null,
461 subHooks: [],
462 },
463 {
@@ -406,6 +465,7 @@ describe('ReactHooksInspectionIntegration', () => {
465 id: 4,
466 name: 'LayoutEffect',
467 value: effect,
468 + debugInfo: null,
469 subHooks: [],
470 },
471 {
@@ -413,6 +473,7 @@ describe('ReactHooksInspectionIntegration', () => {
473 id: 5,
474 name: 'Effect',
475 value: effect,
476 + debugInfo: null,
477 subHooks: [],
478 },
479 {
@@ -420,6 +481,7 @@ describe('ReactHooksInspectionIntegration', () => {
481 id: 6,
482 name: 'ImperativeHandle',
483 value: outsideRef.current,
484 + debugInfo: null,
485 subHooks: [],
486 },
487 {
@@ -427,6 +489,7 @@ describe('ReactHooksInspectionIntegration', () => {
489 id: 7,
490 name: 'Memo',
491 value: 'Ab',
492 + debugInfo: null,
493 subHooks: [],
494 },
495 {
@@ -434,6 +497,7 @@ describe('ReactHooksInspectionIntegration', () => {
497 id: 8,
498 name: 'Callback',
499 value: updateStates,
500 + debugInfo: null,
501 subHooks: [],
502 },
503 ]);
@@ -458,6 +522,7 @@ describe('ReactHooksInspectionIntegration', () => {
522 id: null,
523 name: 'Context',
524 value: 'contextual',
525 + debugInfo: null,
526 subHooks: [],
527 },
528 ]);
@@ -480,6 +545,7 @@ describe('ReactHooksInspectionIntegration', () => {
545 id: 0,
546 name: 'ImperativeHandle',
547 value: obj,
548 + debugInfo: null,
549 subHooks: [],
550 },
551 ]);
@@ -501,6 +567,7 @@ describe('ReactHooksInspectionIntegration', () => {
567 id: 0,
568 name: 'State',
569 value: 'hello',
570 + debugInfo: null,
571 subHooks: [],
572 },
573 ]);
@@ -524,12 +591,14 @@ describe('ReactHooksInspectionIntegration', () => {
591 id: null,
592 name: 'Custom',
593 value: undefined,
594 + debugInfo: null,
595 subHooks: [
596 {
597 isStateEditable: true,
598 id: 0,
599 name: 'State',
600 value: 'hello',
601 + debugInfo: null,
602 subHooks: [],
603 },
604 ],
@@ -553,6 +622,7 @@ describe('ReactHooksInspectionIntegration', () => {
622 isStateEditable: false,
623 name: 'Transition',
624 value: undefined,
625 + debugInfo: null,
626 subHooks: [],
627 },
628 {
@@ -560,6 +630,7 @@ describe('ReactHooksInspectionIntegration', () => {
630 isStateEditable: false,
631 name: 'Memo',
632 value: 'hello',
633 + debugInfo: null,
634 subHooks: [],
635 },
636 {
@@ -567,6 +638,7 @@ describe('ReactHooksInspectionIntegration', () => {
638 isStateEditable: false,
639 name: 'Memo',
640 value: 'not used',
641 + debugInfo: null,
642 subHooks: [],
643 },
644 ]);
@@ -588,6 +660,7 @@ describe('ReactHooksInspectionIntegration', () => {
660 isStateEditable: false,
661 name: 'DeferredValue',
662 value: 'abc',
663 + debugInfo: null,
664 subHooks: [],
665 },
666 {
@@ -595,6 +668,7 @@ describe('ReactHooksInspectionIntegration', () => {
668 isStateEditable: false,
669 name: 'Memo',
670 value: 1,
671 + debugInfo: null,
672 subHooks: [],
673 },
674 {
@@ -602,6 +676,7 @@ describe('ReactHooksInspectionIntegration', () => {
676 isStateEditable: false,
677 name: 'Memo',
678 value: 2,
679 + debugInfo: null,
680 subHooks: [],
681 },
682 ]);
@@ -630,6 +705,7 @@ describe('ReactHooksInspectionIntegration', () => {
705 isStateEditable: true,
706 name: 'State',
707 value: 'hello',
708 + debugInfo: null,
709 subHooks: [],
710 });
711 });
@@ -721,12 +797,14 @@ describe('ReactHooksInspectionIntegration', () => {
797 id: null,
798 name: 'LabeledValue',
799 value: __DEV__ ? 'custom label a' : undefined,
800 + debugInfo: null,
801 subHooks: [
802 {
803 isStateEditable: true,
804 id: 0,
805 name: 'State',
806 value: 'a',
807 + debugInfo: null,
808 subHooks: [],
809 },
810 ],
@@ -736,6 +814,7 @@ describe('ReactHooksInspectionIntegration', () => {
814 id: 1,
815 name: 'State',
816 value: 'b',
817 + debugInfo: null,
818 subHooks: [],
819 },
820 {
@@ -743,12 +822,14 @@ describe('ReactHooksInspectionIntegration', () => {
822 id: null,
823 name: 'Anonymous',
824 value: undefined,
825 + debugInfo: null,
826 subHooks: [
827 {
828 isStateEditable: true,
829 id: 2,
830 name: 'State',
831 value: 'c',
832 + debugInfo: null,
833 subHooks: [],
834 },
835 ],
@@ -758,12 +839,14 @@ describe('ReactHooksInspectionIntegration', () => {
839 id: null,
840 name: 'LabeledValue',
841 value: __DEV__ ? 'custom label d' : undefined,
842 + debugInfo: null,
843 subHooks: [
844 {
845 isStateEditable: true,
846 id: 3,
847 name: 'State',
848 value: 'd',
849 + debugInfo: null,
850 subHooks: [],
851 },
852 ],
@@ -793,18 +876,21 @@ describe('ReactHooksInspectionIntegration', () => {
876 id: null,
877 name: 'Outer',
878 value: __DEV__ ? 'outer' : undefined,
879 + debugInfo: null,
880 subHooks: [
881 {
882 isStateEditable: false,
883 id: null,
884 name: 'Inner',
885 value: __DEV__ ? 'inner' : undefined,
886 + debugInfo: null,
887 subHooks: [
888 {
889 isStateEditable: true,
890 id: 0,
891 name: 'State',
892 value: 0,
893 + debugInfo: null,
894 subHooks: [],
895 },
896 ],
@@ -840,12 +926,14 @@ describe('ReactHooksInspectionIntegration', () => {
926 id: null,
927 name: 'SingleLabelCustom',
928 value: __DEV__ ? 'single one' : undefined,
929 + debugInfo: null,
930 subHooks: [
931 {
932 isStateEditable: true,
933 id: 0,
934 name: 'State',
935 value: 0,
936 + debugInfo: null,
937 subHooks: [],
938 },
939 ],
@@ -855,12 +943,14 @@ describe('ReactHooksInspectionIntegration', () => {
943 id: null,
944 name: 'MultiLabelCustom',
945 value: __DEV__ ? ['one', 'two', 'three'] : undefined,
946 + debugInfo: null,
947 subHooks: [
948 {
949 isStateEditable: true,
950 id: 1,
951 name: 'State',
952 value: 0,
953 + debugInfo: null,
954 subHooks: [],
955 },
956 ],
@@ -870,12 +960,14 @@ describe('ReactHooksInspectionIntegration', () => {
960 id: null,
961 name: 'SingleLabelCustom',
962 value: __DEV__ ? 'single two' : undefined,
963 + debugInfo: null,
964 subHooks: [
965 {
966 isStateEditable: true,
967 id: 2,
968 name: 'State',
969 value: 0,
970 + debugInfo: null,
971 subHooks: [],
972 },
973 ],
@@ -912,11 +1004,13 @@ describe('ReactHooksInspectionIntegration', () => {
1004 id: null,
1005 name: 'Custom',
1006 value: __DEV__ ? 'bar:123' : undefined,
1007 + debugInfo: null,
1008 subHooks: [
1009 {
1010 isStateEditable: true,
1011 id: 0,
1012 name: 'State',
1013 + debugInfo: null,
1014 subHooks: [],
1015 value: 0,
1016 },
@@ -963,6 +1057,7 @@ describe('ReactHooksInspectionIntegration', () => {
1057 id: 0,
1058 name: 'State',
1059 value: 'def',
1060 + debugInfo: null,
1061 subHooks: [],
1062 },
1063 ]);
@@ -1056,6 +1151,7 @@ describe('ReactHooksInspectionIntegration', () => {
1151 id: null,
1152 name: 'Context',
1153 value: 1,
1154 + debugInfo: null,
1155 subHooks: [],
1156 },
1157 {
@@ -1063,6 +1159,7 @@ describe('ReactHooksInspectionIntegration', () => {
1159 id: 0,
1160 name: 'State',
1161 value: {count: 2},
1162 + debugInfo: null,
1163 subHooks: [],
1164 },
1165 ]);
@@ -1089,6 +1186,7 @@ describe('ReactHooksInspectionIntegration', () => {
1186 isStateEditable: false,
1187 name: 'SyncExternalStore',
1188 value: 'snapshot',
1189 + debugInfo: null,
1190 subHooks: [],
1191 },
1192 {
@@ -1096,6 +1194,7 @@ describe('ReactHooksInspectionIntegration', () => {
1194 isStateEditable: false,
1195 name: 'Memo',
1196 value: 'memo',
1197 + debugInfo: null,
1198 subHooks: [],
1199 },
1200 {
@@ -1103,6 +1202,7 @@ describe('ReactHooksInspectionIntegration', () => {
1202 isStateEditable: false,
1203 name: 'Memo',
1204 value: 'not used',
1205 + debugInfo: null,
1206 subHooks: [],
1207 },
1208 ]);
@@ -1125,8 +1225,9 @@ describe('ReactHooksInspectionIntegration', () => {
1225 {
1226 id: null,
1227 isStateEditable: false,
1128 - name: 'Use',
1228 + name: 'Context',
1229 value: 'default',
1230 + debugInfo: null,
1231 subHooks: [],
1232 },
1233 {
@@ -1134,6 +1235,7 @@ describe('ReactHooksInspectionIntegration', () => {
1235 isStateEditable: false,
1236 name: 'Memo',
1237 value: 'memo',
1238 + debugInfo: null,
1239 subHooks: [],
1240 },
1241 {
@@ -1141,6 +1243,7 @@ describe('ReactHooksInspectionIntegration', () => {
1243 isStateEditable: false,
1244 name: 'Memo',
1245 value: 'not used',
1246 + debugInfo: null,
1247 subHooks: [],
1248 },
1249 ]);
@@ -1165,6 +1268,7 @@ describe('ReactHooksInspectionIntegration', () => {
1268 isStateEditable: false,
1269 name: 'Optimistic',
1270 value: 'abc',
1271 + debugInfo: null,
1272 subHooks: [],
1273 },
1274 {
@@ -1172,6 +1276,7 @@ describe('ReactHooksInspectionIntegration', () => {
1276 isStateEditable: false,
1277 name: 'Memo',
1278 value: 'memo',
1279 + debugInfo: null,
1280 subHooks: [],
1281 },
1282 {
@@ -1179,6 +1284,7 @@ describe('ReactHooksInspectionIntegration', () => {
1284 isStateEditable: false,
1285 name: 'Memo',
1286 value: 'not used',
1287 + debugInfo: null,
1288 subHooks: [],
1289 },
1290 ]);
@@ -1205,6 +1311,7 @@ describe('ReactHooksInspectionIntegration', () => {
1311 isStateEditable: false,
1312 name: 'FormState',
1313 value: 0,
1314 + debugInfo: null,
1315 subHooks: [],
1316 },
1317 {
@@ -1212,6 +1319,7 @@ describe('ReactHooksInspectionIntegration', () => {
1319 isStateEditable: false,
1320 name: 'Memo',
1321 value: 'memo',
1322 + debugInfo: null,
1323 subHooks: [],
1324 },
1325 {
@@ -1219,6 +1327,7 @@ describe('ReactHooksInspectionIntegration', () => {
1327 isStateEditable: false,
1328 name: 'Memo',
1329 value: 'not used',
1330 + debugInfo: null,
1331 subHooks: [],
1332 },
1333 ]);
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+11
@@ -198,6 +198,7 @@ describe('InspectedElement', () => {
198 "events": undefined,
199 "hooks": [
200 {
201 + "debugInfo": null,
202 "hookSource": {
203 "columnNumber": "removed by Jest serializer",
204 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -240,6 +241,7 @@ describe('InspectedElement', () => {
241 "events": undefined,
242 "hooks": [
243 {
244 + "debugInfo": null,
245 "hookSource": {
246 "columnNumber": "removed by Jest serializer",
247 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1157,6 +1159,7 @@ describe('InspectedElement', () => {
1159 expect(inspectedElement.hooks).toMatchInlineSnapshot(`
1160 [
1161 {
1162 + "debugInfo": null,
1163 "hookSource": {
1164 "columnNumber": "removed by Jest serializer",
1165 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1184,6 +1187,7 @@ describe('InspectedElement', () => {
1187 expect(inspectedElement.hooks).toMatchInlineSnapshot(`
1188 [
1189 {
1190 + "debugInfo": null,
1191 "hookSource": {
1192 "columnNumber": "removed by Jest serializer",
1193 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1659,6 +1663,7 @@ describe('InspectedElement', () => {
1663 "events": undefined,
1664 "hooks": [
1665 {
1666 + "debugInfo": null,
1667 "hookSource": {
1668 "columnNumber": "removed by Jest serializer",
1669 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1700,6 +1705,7 @@ describe('InspectedElement', () => {
1705 "events": undefined,
1706 "hooks": [
1707 {
1708 + "debugInfo": null,
1709 "hookSource": {
1710 "columnNumber": "removed by Jest serializer",
1711 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1945,6 +1951,7 @@ describe('InspectedElement', () => {
1951 expect(hooks).toMatchInlineSnapshot(`
1952 [
1953 {
1954 + "debugInfo": null,
1955 "hookSource": {
1956 "columnNumber": "removed by Jest serializer",
1957 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -1956,6 +1963,7 @@ describe('InspectedElement', () => {
1963 "name": "DebuggableHook",
1964 "subHooks": [
1965 {
1966 + "debugInfo": null,
1967 "hookSource": {
1968 "columnNumber": "removed by Jest serializer",
1969 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -2239,6 +2247,7 @@ describe('InspectedElement', () => {
2247 {
2248 "hooks": [
2249 {
2250 + "debugInfo": null,
2251 "hookSource": {
2252 "columnNumber": "removed by Jest serializer",
2253 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -2275,6 +2284,7 @@ describe('InspectedElement', () => {
2284 {
2285 "hooks": [
2286 {
2287 + "debugInfo": null,
2288 "hookSource": {
2289 "columnNumber": "removed by Jest serializer",
2290 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",
@@ -2311,6 +2321,7 @@ describe('InspectedElement', () => {
2321 {
2322 "hooks": [
2323 {
2324 + "debugInfo": null,
2325 "hookSource": {
2326 "columnNumber": "removed by Jest serializer",
2327 "fileName": "react-devtools-shared/src/__tests__/inspectedElement-test.js",