Recipes

Fine-grained reactivity

Drag one component on a canvas at 60fps while only that component re-renders — a folded per-frame write plus a per-row `useFragment` subscription, rooted in one live coverage query.

View as Markdown

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