fix(Selection): stop Select's effect from depending on its own write - #359
Merged
Conversation
Select's effect kept the context value in its own deps and returned a self-undoing cleanup, causing an infinite update loop. Now depends only on the stable setter, diffs inside the updater, and selects by isMesh/isLine/isPoints instead of an exact type-string match.
There was a problem hiding this comment.
Pull request overview
This PR fixes an infinite update loop in Select by preventing its effect from depending on a context value that it also updates, and expands “selectable” object detection beyond Mesh to include Line, Points, and Mesh subclasses like SkinnedMesh.
Changes:
- Refactors
Selectto depend on a stableselectsetter and perform add/remove diffs via functional state updates with identity-preserving bailouts. - Introduces
isSelectableto use Three.jsisMesh/isLine/isPointsflags instead of the.typestring. - Adds a new test suite covering loop prevention, selectable object types, cleanup behavior, enabled toggling, and nested
Selectsemantics.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Selection.tsx | Refactors selection effect dependencies and selection diffing; broadens selectable object detection. |
| src/tests/Selection.test.tsx | Adds regression and behavior tests for selection stability, supported object types, cleanup, and nesting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…r+readd Copilot review: Select's effect cleanup unconditionally cleared its claimed objects on every re-run, even when only `children`'s identity changed (not its content), churning the selected array's reference on every unrelated render. Now diffs against live state inside the updater and bails with the same reference when nothing actually changed, while still re-asserting a claim if another Select's update dropped it (nested Selects share objects).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/Selection.tsx:79
- The unmount cleanup always calls
selectwith a new array viafilter, even if none ofstillClaimedare present inprev. That forces an unnecessary state update/rerender (and is inconsistent with the main effect’sreturn prevbail-out). Consider bailing out when nothing is removed.
return () => {
if (!select || !claimed.current.length) return
const stillClaimed = claimed.current
select((prev) => prev.filter((o) => !stillClaimed.includes(o)))
}
src/Selection.tsx:68
- Inside the state updater, membership checks use multiple
Array.prototype.includescalls (prev.includes,current.includes,toRemove.includes), which makes this diff O(n²) for large selections. UsingSetfor membership can keep the diff linear and reduce per-frame work when selecting many objects.
This issue also appears on line 75 of the same file.
const toAdd = current.filter((o) => !prev.includes(o))
const toRemove = previouslyClaimed.filter((o) => !current.includes(o) && prev.includes(o))
if (!toAdd.length && !toRemove.length) return prev
const kept = toRemove.length ? prev.filter((o) => !toRemove.includes(o)) : prev
return toAdd.length ? [...kept, ...toAdd] : kept
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Select's effect kept the context value (
api) in its own dependency array, and returned a cleanup that undoes its own write. Every selection change madeapichange identity via context, which retriggered the effect, which first ran the stale cleanup and undid the write, which retriggered again - an unbounded loop that eventually throws "Maximum update depth exceeded".Fix: depend only on the stable
selectsetter (never changes identity), diff against latest state inside the functional updater instead of a render-scope snapshot, and bail with the same array reference when nothing changed. Also fixes the type check (o.type === 'Mesh'→isMesh || isLine || isPoints) so Line/Points/SkinnedMesh are selectable too.Fixes #260, fixes #330, fixes #236, fixes #134, fixes #134
Supersedes #237, #335, #340, #342 four independent attempts at the same fix, each with a partial gap.
Test plan: 7 new tests - no-loop settling, Line/Points selection, SkinnedMesh, unmount cleanup, enabled toggling, nested
<Select>dedup and OR-semantics.