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:
- an explicit
storeprop — aComposer.createStore()handle, else - 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.