Skip to content

fix(react): defer useSignal update to a microtask to avoid scheduling during commit#2099

Draft
RobHannay wants to merge 2 commits into
clauderic:mainfrom
RobHannay:fix/usesignal-defer-microtask
Draft

fix(react): defer useSignal update to a microtask to avoid scheduling during commit#2099
RobHannay wants to merge 2 commits into
clauderic:mainfrom
RobHannay:fix/usesignal-defer-microtask

Conversation

@RobHannay

@RobHannay RobHannay commented Jul 1, 2026

Copy link
Copy Markdown

Problem

useSignal (used by useComputeduseDragOperation) calls forceUpdate() synchronously from its signal effect. When the observed signal changes while React is mid-commit, that schedules a synchronous React update from within a React lifecycle method.

This happens in an ordinary scenario: a component reads drag state via useDragOperation() and re-renders during a drag (e.g. to show/highlight a drop zone — the use case the docs describe). At drag end, dnd-kit resets the drag operation (DragOperation.reset() clears sourceIdentifier) inside manager.renderer.rendering.then(...), which runs from the React Renderer's layout effect — i.e. during React's commit/effect phase. The useSignal effect then fires forceUpdate() synchronously, producing:

  • Warning: useInsertionEffect must not schedule updates.

Minimal repro shape: any component under DragDropProvider that calls useDragOperation() and re-renders based on source/target logs the warning when a drag ends.

(An earlier version of this description also attributed a NotFoundError: removeChild crash to this path; that turned out to be an application-side issue in how we consumed onDragOver — leaving drag-overs unanswered so OptimisticSortingPlugin reordered DOM the app then also reconciled — not a useSignal bug. This PR is scoped to the warning.)

Fix

Defer the useSignal update to a microtask so it runs after the commit completes instead of scheduling synchronously from within the effect. This mirrors the guard already present in useDeepSignal (queueMicrotask(() => flushSync(forceUpdate))).

useDeepSignal only deferred its sync branch; useSignal's async branch — the one useComputed/useDragOperation use (sync=false) — has the same latent issue, so this defers both branches.

Notes

  • No API change; updates are just flushed a microtask later (imperceptible — React already batches).
  • Nothing in packages/react calls useSignal/useComputed with sync=true, so the sync-branch change has effectively no blast radius.
  • Changeset included (@dnd-kit/react patch).

useSignal called forceUpdate() synchronously from its signal effect. When the
observed signal changes while React is mid-commit - e.g. dnd-kit resets the drag
operation from a layout effect at drag end, observed via useDragOperation - this
scheduled a synchronous React update from within a React lifecycle method,
throwing "useInsertionEffect must not schedule updates" and, when the re-render
was structural, crashing reconciliation with NotFoundError: Node.removeChild.

Defer the update to a microtask (both branches), mirroring the guard already in
useDeepSignal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7d61b68

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@dnd-kit/react Patch
@dnd-kit/abstract Patch
@dnd-kit/collision Patch
@dnd-kit/dom Patch
@dnd-kit/geometry Patch
@dnd-kit/helpers Patch
@dnd-kit/state Patch
@dnd-kit/vue Patch
@dnd-kit/solid Patch
@dnd-kit/svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@RobHannay RobHannay marked this pull request as draft July 1, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant