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 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