Skip to content

Commit e1c7e65

Browse files
author
Brian Vaughn
authored
Update ReactDebugHooks to handle composite hooks (#18130)
The useState hook has always composed the useReducer hook. 1:1 composition like this is fine. But some more recent hooks (e.g. useTransition, useDeferredValue) compose multiple hooks internally. This breaks react-debug-tools because it causes off-by-N errors when the debug tools re-renders the function. For example, if a component were to use the useTransition and useMemo hooks, the normal hooks dispatcher would create a list of first state, then callback, then memo hooks, but the debug tools package would expect a list of transition then memo. This can break user code and cause runtime errors in both the react-debug-tools package and in product code. This PR fixes the currently broken hooks by updating debug tools to be aware of the composite hooks (how many times it should call nextHook essentially) and adds tests to make sure they don't get out of sync again. We'll need to add similar tests for future composite hooks (like useMutableSource #18000).
1 parent d28bd29 commit e1c7e65

File tree

2 files changed

+68
-2
lines changed

2 files changed

+68
-2
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,11 @@ function useResponder(
243243
function useTransition(
244244
config: SuspenseConfig | null | void,
245245
): [(() => void) => void, boolean] {
246-
nextHook();
246+
// useTransition() composes multiple hooks internally.
247+
// Advance the current hook index the same number of times
248+
// so that subsequent hooks have the right memoized state.
249+
nextHook(); // State
250+
nextHook(); // Callback
247251
hookLog.push({
248252
primitive: 'Transition',
249253
stackError: new Error(),
@@ -253,7 +257,11 @@ function useTransition(
253257
}
254258

255259
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
256-
nextHook();
260+
// useDeferredValue() composes multiple hooks internally.
261+
// Advance the current hook index the same number of times
262+
// so that subsequent hooks have the right memoized state.
263+
nextHook(); // State
264+
nextHook(); // Effect
257265
hookLog.push({
258266
primitive: 'DeferredValue',
259267
stackError: new Error(),

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,64 @@ describe('ReactHooksInspectionIntegration', () => {
366366
]);
367367
});
368368

369+
if (__EXPERIMENTAL__) {
370+
it('should support composite useTransition hook', () => {
371+
function Foo(props) {
372+
React.useTransition();
373+
const memoizedValue = React.useMemo(() => 'hello', []);
374+
return <div>{memoizedValue}</div>;
375+
}
376+
let renderer = ReactTestRenderer.create(<Foo />);
377+
let childFiber = renderer.root.findByType(Foo)._currentFiber();
378+
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
379+
expect(tree).toEqual([
380+
{
381+
id: 0,
382+
isStateEditable: false,
383+
name: 'Transition',
384+
value: undefined,
385+
subHooks: [],
386+
},
387+
{
388+
id: 1,
389+
isStateEditable: false,
390+
name: 'Memo',
391+
value: 'hello',
392+
subHooks: [],
393+
},
394+
]);
395+
});
396+
397+
it('should support composite useDeferredValue hook', () => {
398+
function Foo(props) {
399+
React.useDeferredValue('abc', {
400+
timeoutMs: 500,
401+
});
402+
const [state] = React.useState(() => 'hello', []);
403+
return <div>{state}</div>;
404+
}
405+
let renderer = ReactTestRenderer.create(<Foo />);
406+
let childFiber = renderer.root.findByType(Foo)._currentFiber();
407+
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
408+
expect(tree).toEqual([
409+
{
410+
id: 0,
411+
isStateEditable: false,
412+
name: 'DeferredValue',
413+
value: 'abc',
414+
subHooks: [],
415+
},
416+
{
417+
id: 1,
418+
isStateEditable: true,
419+
name: 'State',
420+
value: 'hello',
421+
subHooks: [],
422+
},
423+
]);
424+
});
425+
}
426+
369427
describe('useDebugValue', () => {
370428
it('should support inspectable values for multiple custom hooks', () => {
371429
function useLabeledValue(label) {

0 commit comments

Comments
 (0)