-
Couldn't load subscription status.
- Fork 49.7k
Fix indices of hooks in devtools when using useSyncExternalStore #34547
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
Fix indices of hooks in devtools when using useSyncExternalStore #34547
Conversation
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.
The change makes sense, thank you for working on this.
Please address the comments that I've left, I think we can land this afterwards. Feel free to just tag me on all similar pull requests for other hooks, like useTransition(), useFormState(), useActionState().
| } | ||
| } | ||
| index++; |
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.
Why did you move this increment there?
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.
By mistake, moving it back 👍
| if (next.next !== null) { | ||
| next = next.next; | ||
| prev = prev.next; | ||
| } |
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 probably don't want to check on next.next presence here, it should be implied if the hook object is actually an internal representation of useSyncExternalStore().
I would prefer it to crash instead, if this ever happens, so we spot the gap in our hook parsing logic first.
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.
Makes sense to me as well!
|
Back to you @hoxyq! |
|
As for the other hooks you mentioned, I'm interested in implementing it, though I'm unsure how to reliably identify (during traversal) if a hook is |
|
Thanks for updating the PR! We have a feature that parses hook names, and I believe it is based on hooks indexes. I will double-check if this change actually breaks it, and will follow up here soon. |
Just checking if you had a chance to look into this @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.
Works well, apologies for the delay.
For posterity, we don't use hooks indexes for hook names parsing. We just use the actual stack frame and then try to symbolicate it.
Also, we have 2 different implementations of fetching hook indexes:
- The first one lives in the renderer, which is used by the Profiler tab. This is the one that is updated in this PR.
- The second one lives in
react-debug-tools/src/ReactDebugHooks.js, where we already correctly traverse the hook tree. This logic is used for displaying the "hooks" pane in the InspectedElement view.
useSyncExternalStore shim in react-debug-tools/src/ReactDebugHooks.js:
react/packages/react-debug-tools/src/ReactDebugHooks.js
Lines 462 to 482 in ea0c17b
| function useSyncExternalStore<T>( | |
| subscribe: (() => void) => () => void, | |
| getSnapshot: () => T, | |
| getServerSnapshot?: () => T, | |
| ): T { | |
| // useSyncExternalStore() composes multiple hooks internally. | |
| // Advance the current hook index the same number of times | |
| // so that subsequent hooks have the right memoized state. | |
| nextHook(); // SyncExternalStore | |
| nextHook(); // Effect | |
| const value = getSnapshot(); | |
| hookLog.push({ | |
| displayName: null, | |
| primitive: 'SyncExternalStore', | |
| stackError: new Error(), | |
| value, | |
| debugInfo: null, | |
| dispatcherHookName: 'SyncExternalStore', | |
| }); | |
| return value; | |
| } |
We don't store any identifiers on hooks, so I am not sure if there is a way to distinguish |
|
Thanks for merging!! 🚀 (it's my first) |
After comparing these hooks, it does seem quite difficult to tell them apart 🤔
Could you discuss it with the team, maybe it's worth considering to add hook identifiers/type to each hook? I created a draft PR for this, I think it should work but haven't tested it thoroughly. Let me know what do you think @hoxyq! |
Summary
This PR updates getChangedHooksIndices to account for the fact that useSyncExternalStore internally mounts two hooks, while DevTools should treat it as a single user-facing hook.
It introduces a helper isUseSyncExternalStoreHook to detect this case and adjust iteration so the extra internal hook is skipped when counting changes.
Before:
before.mov
After:
after.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).