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 production — 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. On pointer-up it commits one undoable step:
// 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.