Fix indices of hooks in devtools when using useSyncExternalStore (#34547)

## 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:


https://github.com/user-attachments/assets/0db72a4e-21f7-44c7-ba02-669a272631e5

After:


https://github.com/user-attachments/assets/4da71392-0396-408d-86a7-6fbc82d8c4f5

## How did you test this change?

I used this component to reproduce this issue locally (I followed
instructions in `packages/react-devtools/CONTRIBUTING.md`).

```ts
function Test() {
  // 1
  React.useSyncExternalStore(
    () => {},
    () => {},
    () => {},
  );
  // 2
  const [state, setState] = useState('test'); 
  return (
    <>
      <div
        onClick={() => setState(Math.random())}
        style={{backgroundColor: 'red'}}>
        {state}
      </div>
    </>
  );
}
```
This commit is contained in:
Błażej Kustra 2025-10-21 14:59:20 +02:00 committed by GitHub
parent 613cf80f26
commit 39c6545cef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1913,6 +1913,20 @@ export function attach(
return false;
}
function isUseSyncExternalStoreHook(hookObject: any): boolean {
const queue = hookObject.queue;
if (!queue) {
return false;
}
const boundHasOwnProperty = hasOwnProperty.bind(queue);
return (
boundHasOwnProperty('value') &&
boundHasOwnProperty('getSnapshot') &&
typeof queue.getSnapshot === 'function'
);
}
function isHookThatCanScheduleUpdate(hookObject: any) {
const queue = hookObject.queue;
if (!queue) {
@ -1929,12 +1943,7 @@ export function attach(
return true;
}
// Detect useSyncExternalStore()
return (
boundHasOwnProperty('value') &&
boundHasOwnProperty('getSnapshot') &&
typeof queue.getSnapshot === 'function'
);
return isUseSyncExternalStoreHook(hookObject);
}
function didStatefulHookChange(prev: any, next: any): boolean {
@ -1955,10 +1964,18 @@ export function attach(
const indices = [];
let index = 0;
while (next !== null) {
if (didStatefulHookChange(prev, next)) {
indices.push(index);
}
// useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook
if (isUseSyncExternalStoreHook(next)) {
next = next.next;
prev = prev.next;
}
next = next.next;
prev = prev.next;
index++;