Skip to content

fix(Selection): stop Select's effect from depending on its own write - #359

Merged
kvvasuu merged 4 commits into
masterfrom
fix/selection-update-loop
Aug 2, 2026
Merged

fix(Selection): stop Select's effect from depending on its own write#359
kvvasuu merged 4 commits into
masterfrom
fix/selection-update-loop

Conversation

@kvvasuu

@kvvasuu kvvasuu commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

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 made api change 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 select setter (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.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Select to depend on a stable select setter and perform add/remove diffs via functional state updates with identity-preserving bailouts.
  • Introduces isSelectable to use Three.js isMesh/isLine/isPoints flags instead of the .type string.
  • Adds a new test suite covering loop prevention, selectable object types, cleanup behavior, enabled toggling, and nested Select semantics.

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.

Comment thread src/Selection.tsx Outdated
…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).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 select with a new array via filter, even if none of stillClaimed are present in prev. That forces an unnecessary state update/rerender (and is inconsistent with the main effect’s return prev bail-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.includes calls (prev.includes, current.includes, toRemove.includes), which makes this diff O(n²) for large selections. Using Set for 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

@kvvasuu
kvvasuu merged commit 9377839 into master Aug 2, 2026
1 check passed
@kvvasuu
kvvasuu deleted the fix/selection-update-loop branch August 2, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment