Uh oh!
There was an error while loading. Please reload this page.
Improve the detection of changed hooks - #35123
Conversation
Comparing: 4a3d993...6a9c7bd Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: Expand to show
|
blazejkustra
commented
Nov 13, 2025
@hoxyq I may need your help... Two tests are failing and I don't understand what's wrong
They only fail when I call: constprevHooks=inspectHooks(prevFiber);constnextHooks=inspectHooks(nextFiber);I’ve narrowed it to this: exportfunctioninspectHooks<Props>(renderFunction: (props: Props)=>React$Node,props: Props,currentDispatcher?: CurrentDispatcherRef,): HooksTree{if(currentDispatcher==null){currentDispatcher=ReactSharedInternals;}constpreviousDispatcher=currentDispatcher.H;currentDispatcher.H=DispatcherProxy;letreadHookLog;letancestorStackError;try{ancestorStackError=newError();renderFunction(props);}catch(error){handleRenderFunctionError(error);}finally{readHookLog=hookLog;hookLog=[];// $FlowFixMe[incompatible-use] found when upgrading FlowcurrentDispatcher.H=previousDispatcher;}constrootStack=ancestorStackError===undefined
? ([]: ParsedStackFrame[])
: ErrorStackParser.parse(ancestorStackError);returnbuildTree(rootStack,readHookLog);}I suspect the issue comes from how |
hoxyq
commented
Nov 19, 2025
Looking at the failed tests, it looks like the number of recorded changes doesn't match the number of performed updates. I don't see any early returns in the code, so I am not sure how this may actually happen. I wonder if this could be related to the fact that we are actually calling render functions of components to build the hook tree. |
blazejkustra
commented
Dec 12, 2025
FYI I'm looking into it again, maybe merging main would help 🤞 |
…ok-indexes-mismatch
blazejkustra
commented
Dec 13, 2025
andreisilviudragnea
commented
Dec 25, 2025
Please have a look at #34427 too, maybe it's another solution to this issue @blazejkustra@hoxyq |
I think that merging #34547 without fixing the index counting error for all composite hooks just does not fix the initial problem at all, because having for example a |
@andreisilviudragnea read through the discussion on my previous PR, especially this comment. The plan was to merge it and fix other hooks in follow up PRs. However, it turned out to be harder than expected and I haven't figure it out yet 🫠 |
blazejkustra
commented
Dec 25, 2025
It's a Christmas miracle!! 🎄 After some trial and error I finally realized that due to how Back to you @hoxyq 😄 |
Uh oh!
There was an error while loading. Please reload this page.
| // If the hook has subHooks, flatten them recursively | ||
| if (currentHook.subHooks && currentHook.subHooks.length > 0) { | ||
| flattened.push(...flattenHooksTree(currentHook.subHooks)); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Is there a reason why this specific order is chosen?
This looks like subHooks wills have lower indexes in an array, when flattened. In the UI, the hook numbers (index + 1) usually reflect the tree structure. Basically indexOf(parent) < indexOf(parent.child) is always true.
There was a problem hiding this comment.
flattenHooksTree function performs a depth-first traversal that only keeps leaf hooks (those without subHooks). This means custom hooks are unwrapped to primitive hooks.
For example, given this tree:
[
{ name: 'useCustomHook', subHooks: [
{ name: 'useState', value: 1 },
{ name: 'useEffect' }
]},
{ name: 'useState', value: 2 }
]
The flattened result is:
[
{ name: 'useState', value: 1 }, // index 0
{ name: 'useEffect' }, // index 1
{ name: 'useState', value: 2 } // index 2
]
This order matches React's hooks order, right? So comparing prevFlattened[i] with nextFlattened[i] identifies which primitive hooks changed between renders.
There was a problem hiding this comment.
If we were to change the traversal order the indexes wouldn't be correct I believe, wdyt @hoxyq?
There was a problem hiding this comment.
Thanks for clarifying. I was confused a bit, but that is not an issue, since we only assign indexes to built-in hooks, which should be leaf nodes by definition.
In your example above, I believe this will be the result of flattening:
[
{ name: 'State', value: 1 }, // index 0
{ name: 'Effect' }, // index 1
{ name: 'CustomHook' }, { name: 'State', value: 2 } // index 2
]
There was a problem hiding this comment.
Do you think it still makes sense to do flattening? We could probably just do dfs on both hook trees at the same time.
There was a problem hiding this comment.
I think custom hook would not be there since it is omitted by the continue statement:
if(currentHook.subHooks&¤tHook.subHooks.length>0){flattened.push(...flattenHooksTree(currentHook.subHooks));continue;}There was a problem hiding this comment.
We could probably just do dfs on both hook trees at the same time.
Ahaa, so instead of building prevFlattened and nextFlattened I could do it in place? Good idea, let me try it 👀
hoxyq
commented
Jan 14, 2026
These changes make sense, thank you for improving this. I left some clarifying questions. Could you please also add some tests that will validate these changes? For example, the different (and correct) result of |
…ok-indexes-mismatch
…averse function to directly calculate changed hooks
blazejkustra
commented
Jan 14, 2026
Back to you @hoxyq! I adjusted the code so it traverses in place and added a test as asked 🚀 I tried to run this new test on main and it failed miserably as expected :) |
Uh oh!
There was an error while loading. Please reload this page.
hoxyq
left a comment
There was a problem hiding this comment.
Looks good, please see my suggestion on making iterating over hook trees without optionality.
I can merge after it. We've accumulated some fixes that are not released yet, so I might release new patch / minor release later this month, but no concrete plans yet.
Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
blazejkustra
commented
Jan 14, 2026
I added your suggestions, seems that everything works well. Thank you! |
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
I was wrong lol, good catch! |
inspectHooksOfFiber is too expensive for this use case. On frequently updating components, it can become slow enough to freeze the browser. Revert hook change detection to the pre-react#35123 behavior. Then defer parsing hook metadata, such as hook names, until after the commit completes, and add caching so hook inspection runs at most once per fiber whenever possible. For now, special handling is only implemented for SyncExternalStore, Transition, ActionState, and FormState.
Summary
cc @hoxyq
Fixes#28584. Follow up to PR: #34547
This PR updates getChangedHooksIndices to account for the fact that
useSyncExternalStore,useTransition,useActionState,useFormStateinternally mounts more than one hook while DevTools should treat it as a single user-facing hook.Approach idea came from this comment 😄
Before:
QuickTime.movie.2.mov
After:
QuickTime.movie.mov
How did you test this change?
I used this component to reproduce this issue locally (I followed instructions in
packages/react-devtools/CONTRIBUTING.md).Details