Improve the detection of changed hooks#191
Conversation
Greptile SummaryThis PR refactors hook change detection to better handle composite hooks like Key changes:
Critical issues found:
Confidence Score: 1/5
Important Files Changed
|
| if (isUseSyncExternalStoreHook(next)) { | ||
| next = next.next; | ||
| prev = prev.next; | ||
| if (prevHook === null || nextHook === null) { |
There was a problem hiding this comment.
logic: Check should handle undefined values. If nextFlattened.length < prevFlattened.length, accessing nextFlattened[index] returns undefined, not null, so this check won't catch it.
| if (prevHook === null || nextHook === null) { | |
| if (prevHook == null || nextHook == null) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-shared/src/backend/fiber/renderer.js
Line: 2078:2078
Comment:
**logic:** Check should handle `undefined` values. If `nextFlattened.length < prevFlattened.length`, accessing `nextFlattened[index]` returns `undefined`, not `null`, so this check won't catch it.
```suggestion
if (prevHook == null || nextHook == null) {
```
How can I resolve this? If you propose a fix, please make it concise.| if (prevHook === null || nextHook === null) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
logic: check doesn't handle undefined - when nextFlattened.length < prevFlattened.length, nextFlattened[index] returns undefined, not null
| if (prevHook === null || nextHook === null) { | |
| continue; | |
| } | |
| if (prevHook == null || nextHook == null) { | |
| continue; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-shared/src/backend/fiber/renderer.js
Line: 2078:2080
Comment:
**logic:** check doesn't handle `undefined` - when `nextFlattened.length < prevFlattened.length`, `nextFlattened[index]` returns `undefined`, not `null`
```suggestion
if (prevHook == null || nextHook == null) {
continue;
}
```
How can I resolve this? If you propose a fix, please make it concise.…averse function to directly calculate changed hooks
| indices.push(index); | ||
| } | ||
| function traverse(prevTree: HooksTree, nextTree: HooksTree): void { | ||
| for (let i = 0; i < prevTree.length; i++) { |
There was a problem hiding this comment.
logic: loop should use the longer array's length to handle cases where trees differ in size
| for (let i = 0; i < prevTree.length; i++) { | |
| for (let i = 0; i < Math.max(prevTree.length, nextTree.length); i++) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-shared/src/backend/fiber/renderer.js
Line: 2058:2058
Comment:
**logic:** loop should use the longer array's length to handle cases where trees differ in size
```suggestion
for (let i = 0; i < Math.max(prevTree.length, nextTree.length); i++) {
```
How can I resolve this? If you propose a fix, please make it concise.| const prevHook = prevTree[i]; | ||
| const nextHook = nextTree[i]; | ||
|
|
||
| // useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook | ||
| if (isUseSyncExternalStoreHook(next)) { | ||
| next = next.next; | ||
| prev = prev.next; | ||
| } | ||
| if (prevHook?.subHooks?.length > 0 && nextHook?.subHooks?.length > 0) { | ||
| traverse(prevHook.subHooks, nextHook.subHooks); | ||
| continue; | ||
| } | ||
|
|
||
| next = next.next; | ||
| prev = prev.next; | ||
| index++; | ||
| if (didStatefulHookChange(prevHook, nextHook)) { |
There was a problem hiding this comment.
logic: add guard for when hooks are undefined due to array length mismatch
| const prevHook = prevTree[i]; | |
| const nextHook = nextTree[i]; | |
| // useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook | |
| if (isUseSyncExternalStoreHook(next)) { | |
| next = next.next; | |
| prev = prev.next; | |
| } | |
| if (prevHook?.subHooks?.length > 0 && nextHook?.subHooks?.length > 0) { | |
| traverse(prevHook.subHooks, nextHook.subHooks); | |
| continue; | |
| } | |
| next = next.next; | |
| prev = prev.next; | |
| index++; | |
| if (didStatefulHookChange(prevHook, nextHook)) { | |
| const prevHook = prevTree[i]; | |
| const nextHook = nextTree[i]; | |
| if (!prevHook || !nextHook) { | |
| continue; | |
| } | |
| if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) { | |
| traverse(prevHook.subHooks, nextHook.subHooks); | |
| continue; | |
| } | |
| if (didStatefulHookChange(prevHook, nextHook)) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-shared/src/backend/fiber/renderer.js
Line: 2059:2067
Comment:
**logic:** add guard for when hooks are `undefined` due to array length mismatch
```suggestion
const prevHook = prevTree[i];
const nextHook = nextTree[i];
if (!prevHook || !nextHook) {
continue;
}
if (prevHook.subHooks.length > 0 && nextHook.subHooks.length > 0) {
traverse(prevHook.subHooks, nextHook.subHooks);
continue;
}
if (didStatefulHookChange(prevHook, nextHook)) {
```
How can I resolve this? If you propose a fix, please make it concise.| function traverse(prevTree: HooksTree, nextTree: HooksTree): void { | ||
| for (let i = 0; i < prevTree.length; i++) { | ||
| const prevHook = prevTree[i]; | ||
| const nextHook = nextTree[i]; | ||
|
|
||
| // useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook | ||
| if (isUseSyncExternalStoreHook(next)) { | ||
| next = next.next; | ||
| prev = prev.next; | ||
| } | ||
| if (prevHook?.subHooks?.length > 0 && nextHook?.subHooks?.length > 0) { | ||
| traverse(prevHook.subHooks, nextHook.subHooks); | ||
| continue; | ||
| } | ||
|
|
||
| next = next.next; | ||
| prev = prev.next; | ||
| index++; | ||
| if (didStatefulHookChange(prevHook, nextHook)) { | ||
| indices.push(index); | ||
| } | ||
|
|
||
| index++; | ||
| } |
There was a problem hiding this comment.
logic: runtime error when nextTree has fewer hooks than prevTree - accessing properties on undefined
when i >= nextTree.length, nextHook becomes undefined, and didStatefulHookChange(prevHook, nextHook) will throw trying to access next.isStateEditable or next.name
also misses changes when nextTree.length > prevTree.length since loop only iterates to prevTree.length
| function traverse(prevTree: HooksTree, nextTree: HooksTree): void { | |
| for (let i = 0; i < prevTree.length; i++) { | |
| const prevHook = prevTree[i]; | |
| const nextHook = nextTree[i]; | |
| // useSyncExternalStore creates 2 internal hooks, but we only count it as 1 user-facing hook | |
| if (isUseSyncExternalStoreHook(next)) { | |
| next = next.next; | |
| prev = prev.next; | |
| } | |
| if (prevHook?.subHooks?.length > 0 && nextHook?.subHooks?.length > 0) { | |
| traverse(prevHook.subHooks, nextHook.subHooks); | |
| continue; | |
| } | |
| next = next.next; | |
| prev = prev.next; | |
| index++; | |
| if (didStatefulHookChange(prevHook, nextHook)) { | |
| indices.push(index); | |
| } | |
| index++; | |
| } | |
| function traverse(prevTree: HooksTree, nextTree: HooksTree): void { | |
| const maxLength = Math.max(prevTree.length, nextTree.length); | |
| for (let i = 0; i < maxLength; i++) { | |
| const prevHook = prevTree[i]; | |
| const nextHook = nextTree[i]; | |
| if (!prevHook || !nextHook) { | |
| // Array length mismatch - treat as a change | |
| indices.push(index); | |
| index++; | |
| continue; | |
| } | |
| if (prevHook.subHooks?.length > 0 && nextHook.subHooks?.length > 0) { | |
| traverse(prevHook.subHooks, nextHook.subHooks); | |
| continue; | |
| } | |
| if (didStatefulHookChange(prevHook, nextHook)) { | |
| indices.push(index); | |
| } | |
| index++; | |
| } | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-shared/src/backend/fiber/renderer.js
Line: 2057:2072
Comment:
**logic:** runtime error when `nextTree` has fewer hooks than `prevTree` - accessing properties on `undefined`
when `i >= nextTree.length`, `nextHook` becomes `undefined`, and `didStatefulHookChange(prevHook, nextHook)` will throw trying to access `next.isStateEditable` or `next.name`
also misses changes when `nextTree.length > prevTree.length` since loop only iterates to `prevTree.length`
```suggestion
function traverse(prevTree: HooksTree, nextTree: HooksTree): void {
const maxLength = Math.max(prevTree.length, nextTree.length);
for (let i = 0; i < maxLength; i++) {
const prevHook = prevTree[i];
const nextHook = nextTree[i];
if (!prevHook || !nextHook) {
// Array length mismatch - treat as a change
indices.push(index);
index++;
continue;
}
if (prevHook.subHooks?.length > 0 && nextHook.subHooks?.length > 0) {
traverse(prevHook.subHooks, nextHook.subHooks);
continue;
}
if (didStatefulHookChange(prevHook, nextHook)) {
indices.push(index);
}
index++;
}
}
```
How can I resolve this? If you propose a fix, please make it concise.Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
| for (let i = 0; i < prevTree.length; i++) { | ||
| const prevHook = prevTree[i]; | ||
| const nextHook = nextTree[i]; |
There was a problem hiding this comment.
logic: loop must use Math.max(prevTree.length, nextTree.length) to handle trees of different sizes
when i >= nextTree.length, nextHook is undefined, causing runtime error in didStatefulHookChange when accessing next.isStateEditable or next.name
also misses changes when nextTree.length > prevTree.length since loop only iterates to prevTree.length
| for (let i = 0; i < prevTree.length; i++) { | |
| const prevHook = prevTree[i]; | |
| const nextHook = nextTree[i]; | |
| for (let i = 0; i < Math.max(prevTree.length, nextTree.length); i++) { | |
| const prevHook = prevTree[i]; | |
| const nextHook = nextTree[i]; |
Are hook trees expected to always have the same length between renders, or can components conditionally call different numbers of hooks?
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-shared/src/backend/fiber/renderer.js
Line: 2058:2060
Comment:
**logic:** loop must use `Math.max(prevTree.length, nextTree.length)` to handle trees of different sizes
when `i >= nextTree.length`, `nextHook` is `undefined`, causing runtime error in `didStatefulHookChange` when accessing `next.isStateEditable` or `next.name`
also misses changes when `nextTree.length > prevTree.length` since loop only iterates to `prevTree.length`
```suggestion
for (let i = 0; i < Math.max(prevTree.length, nextTree.length); i++) {
const prevHook = prevTree[i];
const nextHook = nextTree[i];
```
Are hook trees expected to always have the same length between renders, or can components conditionally call different numbers of hooks?
How can I resolve this? If you propose a fix, please make it concise.| if (prevHook.subHooks?.length > 0 && nextHook.subHooks?.length > 0) { | ||
| traverse(prevHook.subHooks, nextHook.subHooks); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
logic: doesn't handle when prevHook or nextHook is undefined due to mismatched tree lengths
accessing .subHooks on undefined will throw
| if (prevHook.subHooks?.length > 0 && nextHook.subHooks?.length > 0) { | |
| traverse(prevHook.subHooks, nextHook.subHooks); | |
| continue; | |
| } | |
| if (prevHook?.subHooks?.length > 0 && nextHook?.subHooks?.length > 0) { | |
| traverse(prevHook.subHooks, nextHook.subHooks); | |
| continue; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-shared/src/backend/fiber/renderer.js
Line: 2062:2065
Comment:
**logic:** doesn't handle when `prevHook` or `nextHook` is `undefined` due to mismatched tree lengths
accessing `.subHooks` on `undefined` will throw
```suggestion
if (prevHook?.subHooks?.length > 0 && nextHook?.subHooks?.length > 0) {
traverse(prevHook.subHooks, nextHook.subHooks);
continue;
}
```
How can I resolve this? If you propose a fix, please make it concise.| if (didStatefulHookChange(prevHook, nextHook)) { | ||
| indices.push(index); | ||
| } |
There was a problem hiding this comment.
logic: didStatefulHookChange will crash if either hook is undefined
needs guard to handle mismatched tree lengths
| if (didStatefulHookChange(prevHook, nextHook)) { | |
| indices.push(index); | |
| } | |
| if (prevHook && nextHook && didStatefulHookChange(prevHook, nextHook)) { | |
| indices.push(index); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-shared/src/backend/fiber/renderer.js
Line: 2067:2069
Comment:
**logic:** `didStatefulHookChange` will crash if either hook is `undefined`
needs guard to handle mismatched tree lengths
```suggestion
if (prevHook && nextHook && didStatefulHookChange(prevHook, nextHook)) {
indices.push(index);
}
```
How can I resolve this? If you propose a fix, please make it concise.|
Upstream PR was closed or merged. Code is synced via branch mirror. |
Mirror of facebook/react#35123
Original author: blazejkustra
Summary
cc
@hoxyqFixes react/react#28584. Follow up to PR: react/react#34547
This PR updates getChangedHooksIndices to account for the fact that
useSyncExternalStore,useTransition,useActionState,useFormStateinternally mounts more than one hook while DevTools should treat it as a single user-facing hook.Approach idea came from this comment 😄
Before:
QuickTime.movie.2.mov
After:
QuickTime.movie.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).Details