Composer state

One isolated store per Composer, with explicit createStore handles for cross-tree access.

The composer's state — editor content, command lists, ask-user flow, attachments — lives in a store. Every <Composer> creates its own isolated instance, so a single composer works with zero setup and several composers coexist on one page without any wiring.

Resolution

Composer resolves its store once, at mount:

  1. an explicit store prop — a Composer.createStore() handle, else
  2. an instance created for this mount, owned by it and reset on unmount.
// Zero config — an isolated instance. Two of these never interact.
<Composer onSubmit={handleSubmit}>{/* … */}</Composer>

// Explicit — a handle you own, for anything that must reach the
// composer from outside its tree.
const composerStore = Composer.createStore();

<Composer store={composerStore} onSubmit={handleSubmit}>{/* … */}</Composer>

Reading state inside the tree

useComposer(selector) subscribes to a slice of the nearest composer's store. It throws outside a <Composer> — inside the tree, no store is ever named:

const isSubmitting = useComposer((state) => state.isSubmitting);
const attachments = useComposer((state) => state.attachments.items);

useComposerController() is its imperative twin — the nearest composer's editor controls (focus, insert, clear) for components inside the form.

Reaching in from outside

A Composer.createStore() handle carries the full surface. Imperative control needs no hook at all:

composerStore.controller.insertChip({ prefix: "@", value: fileId, label: fileName });
composerStore.controller.focus();

Reactive reads go through useComposerStore(store, selector) — the outside-the-tree twin of useComposer:

const attachments = useComposerStore(composerStore, (state) => state.attachments.items);

Wrap it once per instance so call sites pass only the selector:

// lib/composer.ts — one module owns the instance
export const chatComposerStore = Composer.createStore();

export const useChatComposer = <T,>(selector: (state: ComposerState) => T) =>
  useComposerStore(chatComposerStore, selector);

The rule is deliberate: explicit at the boundary, implicit within it. Inside <Composer>, hooks resolve by context and nothing is named; outside it, someone must say which composer they mean. There is no global fallback — a composer's state can never be read or driven by accident.