From 16e77a3858354fbeb492d980b89af1fbcfd496b8 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sat, 20 Sep 2025 22:23:56 +0200 Subject: [PATCH 1/4] Fix indices of hooks ind evtools when using useSyncExternalStore --- .../src/backend/fiber/renderer.js | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 33786a41877..072da6e1a81 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1943,6 +1943,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 getChangedHooksIndices(prev: any, next: any): null | Array { if (prev == null || next == null) { return null; @@ -1950,13 +1964,30 @@ export function attach( const indices = []; let index = 0; - while (next !== null) { - if (didStatefulHookChange(prev, next)) { - indices.push(index); + + if ( + next.hasOwnProperty('baseState') && + next.hasOwnProperty('memoizedState') && + next.hasOwnProperty('next') && + next.hasOwnProperty('queue') + ) { + 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)) { + if (next.next !== null) { + next = next.next; + prev = prev.next; + } + } + + index++; + next = next.next; + prev = prev.next; } - next = next.next; - prev = prev.next; - index++; } return indices; From 0dd8c608286e17fe0c17c7c485f09e9f7011f8ba Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sat, 20 Sep 2025 22:57:07 +0200 Subject: [PATCH 2/4] Clean the code a bit --- .../src/backend/fiber/renderer.js | 68 ++++++++----------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 072da6e1a81..10ab1dbce19 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1908,6 +1908,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) { @@ -1924,12 +1938,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 { @@ -1943,20 +1952,6 @@ 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 getChangedHooksIndices(prev: any, next: any): null | Array { if (prev == null || next == null) { return null; @@ -1965,29 +1960,22 @@ export function attach( const indices = []; let index = 0; - if ( - next.hasOwnProperty('baseState') && - next.hasOwnProperty('memoizedState') && - next.hasOwnProperty('next') && - next.hasOwnProperty('queue') - ) { - while (next !== null) { - if (didStatefulHookChange(prev, next)) { - indices.push(index); - } + 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)) { - if (next.next !== null) { - next = next.next; - prev = prev.next; - } + // useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook + if (isUseSyncExternalStoreHook(next)) { + if (next.next !== null) { + next = next.next; + prev = prev.next; } - - index++; - next = next.next; - prev = prev.next; } + + index++; + next = next.next; + prev = prev.next; } return indices; From bd16801a69a5a7c84e0b04dd5d3bd4c4b957737a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 15 Oct 2025 15:17:31 +0200 Subject: [PATCH 3/4] Move back line with increment --- packages/react-devtools-shared/src/backend/fiber/renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 10ab1dbce19..7f2c26f09db 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1973,9 +1973,9 @@ export function attach( } } - index++; next = next.next; prev = prev.next; + index++; } return indices; From aa0e1257413f6069e0497b66719879350f81f650 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 15 Oct 2025 15:20:41 +0200 Subject: [PATCH 4/4] Remove next.next presence check in traversal logic --- .../react-devtools-shared/src/backend/fiber/renderer.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 7f2c26f09db..e6d9a124693 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -1967,10 +1967,8 @@ export function attach( // useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook if (isUseSyncExternalStoreHook(next)) { - if (next.next !== null) { - next = next.next; - prev = prev.next; - } + next = next.next; + prev = prev.next; } next = next.next;