Skip to content

Let React Flow own node state so drag frames render inside the pointer event - #327

Merged
dovvnloading merged 1 commit into
mainfrom
refactor/uncontrolled-node-state
Aug 14, 2026
Merged

Let React Flow own node state so drag frames render inside the pointer event#327
dovvnloading merged 1 commit into
mainfrom
refactor/uncontrolled-node-state

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

Working node editors update a dragged node's position and its connection paths synchronously, in the same mousemove handler. Drawflow is a clear reference: its drag handler writes the node's position, then immediately recomputes and writes the d attribute of every affected connection path. No framework state, no deferred work, nothing between the input event and the updated geometry.

This canvas did the opposite. Node state was controlled by the component, so every drag frame travelled through React state, a re-render, and React Flow's own prop-sync effect before the renderer learned the new position. That effect is a passive one, which React runs after the browser has already had the opportunity to paint. Node cards and connection geometry are both produced by React Flow from that state, so the entire canvas was being updated a hop later than the gesture driving it.

Change

React Flow now owns the node collection (defaultNodes). A drag frame is applied to its store synchronously inside the pointer event, and the node card and its connections re-render together from that single store write. The component keeps a mirror of the collection for its own logic - drag corrections, delete routing, scene merging - and pushes backend scene snapshots into the store explicitly, which is not per-frame work.

Combined with the drag corrections already running inside React Flow's change pipeline (#326), the position a gesture produces is now computed once and rendered once.

Also adds web_ui/src/app/canvas/drag/dragCorrections.ts: the drag correction maths - speed factor, smart-guide snap, group cascade - extracted as a pure, framework-free module with an explicit contract, so the rule "one position, computed once" lives in one reviewable place rather than inline in a component.

Test plan

  • Full frontend check passes: schema, types, lint (0 errors), 1929/1929 tests, production build, bundle size
  • On the affected machine: drag a connected node slowly and confirm the connection stays attached

…r event

Working node editors update a dragged node's position and the connection
paths attached to it synchronously, in the same mousemove handler.
Drawflow is a clear reference: its drag handler writes the node's position
and then immediately recomputes and writes every affected connection's
path, with no framework state and no deferred work in between.

This canvas did the opposite. Node state was controlled, so every drag
frame travelled through this component's React state, a re-render, and
React Flow's own prop-sync effect - a passive effect, which React runs
after the browser has already had the opportunity to paint - before the
renderer learned the new position. Node cards and connection geometry are
both produced by React Flow from that state, so the whole canvas was
being updated a hop later than the gesture that drove it.

React Flow now owns the node collection (defaultNodes). A drag frame is
applied to its store synchronously inside the pointer event, and the node
card and its connections re-render together from that single write. The
component keeps a mirror of the collection for its own logic - drag
corrections, delete routing, scene merging - and pushes backend scene
snapshots into the store explicitly, which is not per-frame work.

Combined with the drag corrections already running inside React Flow's
change pipeline, the position a gesture produces is now computed once and
rendered once, with nothing between the pointer event and the frame.

Also adds web_ui/src/app/canvas/drag/dragCorrections.ts: the drag
correction maths (speed factor, smart-guide snap, group cascade) as a
pure, framework-free module with an explicit contract, so the rule "one
position, computed once" is enforceable in one reviewable place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit 3738231 into main Aug 14, 2026
@dovvnloading
dovvnloading deleted the refactor/uncontrolled-node-state branch August 15, 2026 14:41
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