@samitouri / QOS-React / commits / 39c6545cef

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> </> ); } ```

Błażej Kustra committed Oct 21, 2025 at 14:59 UTC 39c6545cef85b5251e519080fd315bff728d87de
1 file changed +23 -6
packages/react-devtools-shared/src/backend/fiber/renderer.js
+23 -6
@@ -1913,6 +1913,20 @@ export function attach(
1913 return false;
1914 }
1915
1916 + function isUseSyncExternalStoreHook(hookObject: any): boolean {
1917 + const queue = hookObject.queue;
1918 + if (!queue) {
1919 + return false;
1920 + }
1921 +
1922 + const boundHasOwnProperty = hasOwnProperty.bind(queue);
1923 + return (
1924 + boundHasOwnProperty('value') &&
1925 + boundHasOwnProperty('getSnapshot') &&
1926 + typeof queue.getSnapshot === 'function'
1927 + );
1928 + }
1929 +
1930 function isHookThatCanScheduleUpdate(hookObject: any) {
1931 const queue = hookObject.queue;
1932 if (!queue) {
@@ -1929,12 +1943,7 @@ export function attach(
1943 return true;
1944 }
1945
1932 - // Detect useSyncExternalStore()
1933 - return (
1934 - boundHasOwnProperty('value') &&
1935 - boundHasOwnProperty('getSnapshot') &&
1936 - typeof queue.getSnapshot === 'function'
1937 - );
1946 + return isUseSyncExternalStoreHook(hookObject);
1947 }
1948
1949 function didStatefulHookChange(prev: any, next: any): boolean {
@@ -1955,10 +1964,18 @@ export function attach(
1964
1965 const indices = [];
1966 let index = 0;
1967 +
1968 while (next !== null) {
1969 if (didStatefulHookChange(prev, next)) {
1970 indices.push(index);
1971 }
1972 +
1973 + // useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook
1974 + if (isUseSyncExternalStoreHook(next)) {
1975 + next = next.next;
1976 + prev = prev.next;
1977 + }
1978 +
1979 next = next.next;
1980 prev = prev.next;
1981 index++;