-
Notifications
You must be signed in to change notification settings - Fork 50.4k
Improve the detection of changed hooks #35123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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
|
|
@hoxyq I may need your help... Two tests are failing and I don't understand what's wrong
They only fail when I call: const prevHooks = inspectHooks(prevFiber);
const nextHooks = inspectHooks(nextFiber);I’ve narrowed it to this: export function inspectHooks<Props>(
renderFunction: (props: Props) => React$Node,
props: Props,
currentDispatcher?: CurrentDispatcherRef,
): HooksTree {
if (currentDispatcher == null) {
currentDispatcher = ReactSharedInternals;
}
const previousDispatcher = currentDispatcher.H;
currentDispatcher.H = DispatcherProxy;
let readHookLog;
let ancestorStackError;
try {
ancestorStackError = new Error();
renderFunction(props);
} catch (error) {
handleRenderFunctionError(error);
} finally {
readHookLog = hookLog;
hookLog = [];
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentDispatcher.H = previousDispatcher;
}
const rootStack =
ancestorStackError === undefined
? ([]: ParsedStackFrame[])
: ErrorStackParser.parse(ancestorStackError);
return buildTree(rootStack, readHookLog);
}I suspect the issue comes from how |
|
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. |
|
FYI I'm looking into it again, maybe merging main would help 🤞 |
…ok-indexes-mismatch
|
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 🫠 |
|
It's a Christmas miracle!! 🎄 After some trial and error I finally realized that due to how Back to you @hoxyq 😄 |
| // 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think custom hook would not be there since it is omitted by the continue statement:
if (currentHook.subHooks && currentHook.subHooks.length > 0) {
flattened.push(...flattenHooksTree(currentHook.subHooks));
continue;
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👀
|
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
|
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 :) |
hoxyq
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 <[email protected]>
|
I added your suggestions, seems that everything works well. Thank you! |
Co-authored-by: Ruslan Lesiutin <[email protected]>
|
I was wrong lol, good catch! |
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