Dragging an object on a canvas fires a write every frame. The trick to keeping it smooth is where
each subscription lives: one root query syncs the whole subtree, but each item subscribes to its
own row with useFragment — so a write to one row re-renders one component, not the list.
In the wild — Strut
On Strut’s slide canvas, one query syncs the
deck → slides → components tree, declared with nested fragments:
// shared/fragments.ts — the slide's components, as z-ordered masked refs
export const SlideFragment = defineFragment(slide, (f) =>
f.sub('components', rels.slideComponents, ComponentFragment, (t) =>
t.orderBy('z_order', 'asc'),
),
)
shared/fragments.ts L43–47 · Strut — rooted once with useRoot(deckDetailQuery, { deckId }) (deck.$deckId.tsx L73).
Each component renders through its own useFragment — a masked, per-row subscription. When one
component’s row changes, only this reader’s data changes:
// src/editor/componentFragments.tsx — one subscription per component
export function ComponentDataReader({ component, onData, onRemove, children }: ComponentDataReaderProps) {
const data = useFragment(ComponentFragment, component.ref)
if (!data) return null
return (
<ComponentData data={data} onData={onData} onRemove={onRemove}>
{children}
</ComponentData>
)
}
src/editor/componentFragments.tsx L29–47 · Strut
The pointer-move loop writes the new position through moveComponent.folded on every frame (see
folded mutations), keyed per component so the burst coalesces to the
server, then commits one undoable step on pointer-up:
// src/editor/Stage.tsx — inside the pointermove handler
for (const frame of lastFrames) {
mutate.moveComponent.folded(
{ key: frame.id },
{ id: frame.id, x: frame.x, y: frame.y },
)
}
src/editor/Stage.tsx L706–711 · Strut — the mutator is a plain patch: yield tx.update('component', { id: a.id, x: a.x, y: a.y }) (shared/app-def.ts L738–743).
That split — coverage query at the root, useFragment at each leaf — is what makes a drag update
the dragged object live without re-rendering its siblings.
See also
- Folded mutations — the per-frame write side of the same interaction.
- Compose the UI with fragments — declaring per-component data and rooting it once.