Skip to content

Conversation

@blazejkustra
Copy link
Contributor

@blazejkustra blazejkustra commented Sep 20, 2025

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).

function Test() {
  // 1
  React.useSyncExternalStore(
    () => {},
    () => {},
    () => {},
  );
  // 2
  const [state, setState] = useState('test'); 
  return (
    <>
      <div
        onClick={() => setState(Math.random())}
        style={{backgroundColor: 'red'}}>
        {state}
      </div>
    </>
  );
}

@blazejkustra blazejkustra marked this pull request as ready for review September 22, 2025 17:38
@hoxyq hoxyq self-requested a review October 15, 2025 02:39
Copy link
Contributor

@hoxyq hoxyq left a 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++;
Copy link
Contributor

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?

Copy link
Contributor Author

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 👍

Comment on lines 1970 to 1973
if (next.next !== null) {
next = next.next;
prev = prev.next;
}
Copy link
Contributor

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.

Copy link
Contributor Author

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!

@blazejkustra blazejkustra requested a review from hoxyq October 15, 2025 13:21
@blazejkustra
Copy link
Contributor Author

Back to you @hoxyq!

@blazejkustra
Copy link
Contributor Author

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 useTransition, useFormState, or useActionState. If you have any guidance, please let me know 😄

@hoxyq
Copy link
Contributor

hoxyq commented Oct 16, 2025

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.

@blazejkustra
Copy link
Contributor Author

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

Copy link
Contributor

@hoxyq hoxyq left a 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:

  1. The first one lives in the renderer, which is used by the Profiler tab. This is the one that is updated in this PR.
  2. 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:

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;
}

@hoxyq
Copy link
Contributor

hoxyq commented Oct 21, 2025

I'm unsure how to reliably identify (during traversal) if a hook is useTransition, useFormState, or useActionState

We don't store any identifiers on hooks, so I am not sure if there is a way to distinguish useTransition() from useState(); useCallback(). I will take a look once I have more time.

@hoxyq hoxyq merged commit 39c6545 into facebook:main Oct 21, 2025
240 of 241 checks passed
@blazejkustra
Copy link
Contributor Author

Thanks for merging!! 🚀 (it's my first)

@blazejkustra
Copy link
Contributor Author

blazejkustra commented Oct 21, 2025

I am not sure if there is a way to distinguish useTransition() from useState(); useCallback()

After comparing these hooks, it does seem quite difficult to tell them apart 🤔

We don't store any identifiers on hooks

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants