Controllable state
Every piece of interactive state in every component — a viewport, a selection, a row order, a tree’s arrangement, panel sizes — follows the same contract. Each is a group of four props named for the state:
| Prop | Purpose |
|---|---|
viewport |
The current value. Present → controlled: you own it. |
defaultViewport |
The starting value when uncontrolled: the component owns it. |
onViewportChange |
Called on every change, in every mode. |
viewportStore |
Hand the state to an external store the component reads and writes directly. |
The same four exist as selection/defaultSelection/onSelectionChange/selectionStore,
rowOrder/…, panelSizes/…, and so on. Once you know one, you know them all.
Uncontrolled
Section titled “Uncontrolled”The component keeps the state; you can still observe every change.
<MultipleSequenceAlignment msa={msa} onSelectionChange={(s) => console.log(s.columns)} />Controlled
Section titled “Controlled”You keep the state, exactly like a controlled <input>. The component only reports changes —
nothing visibly changes until you pass the new value back. Use this to validate, transform, or
record changes (an undo stack, say).
const [selection, setSelection] = useState({ rows: [], columns: [] });
<MultipleSequenceAlignment msa={msa} selection={selection} onSelectionChange={setSelection} />;External store
Section titled “External store”For state shared across components or with the rest of an app, pass a StoreController — three
functions, getValue/setValue/subscribe, that any store can implement.
createControllableStore makes a standalone one:
import { createControllableStore, type Viewport } from "react-bio-viz";
const viewportStore = createControllableStore<Viewport>(initialViewport);
<MultipleSequenceAlignment msa={msa} viewportStore={viewportStore} /><MultipleSequenceAlignment msa={msa} viewportStore={viewportStore} /> {/* same window */}
viewportStore.setValue((v) => ({ ...v, x0: 100, x1: 100 + (v.x1 - v.x0) }));Below, two alignments share one viewport store — pan or zoom either and the other follows; the button writes to the store directly:
To keep the state in an existing Zustand store, bind one slice of it with
createZustandStoreController(appStore, (s) => s.msaViewport, (store, next) => …). The
Jupyter widgets use the same seam — each synced traitlet is a StoreController —
which is why no component contains any Jupyter-specific code.
Simple components
Section titled “Simple components”Every component comes in two layers. SimplePhyloTree, SimpleMultipleSequenceAlignment and the
other Simple* components are the bare views, fully controlled: they keep no state, so each
interaction only takes effect once you pass the reported value back. Omit a value to show its
default — the whole gene, the top of the tree — until the first interaction reports one:
const [viewport, setViewport] = useState<Viewport>();
<SimplePhyloTree tree={tree} viewport={viewport} onViewportChange={setViewport} />The components without the prefix wrap them: they add the pan/zoom toolbar (and the BLAST metric
selector), and hold each piece of state in one of the three modes above — uncontrolled state lives
in a private Zustand store. Reach for a Simple* component to build your own navigation around a
view; ViewportToolbar is exported for reuse.
Data edits are events, not state
Section titled “Data edits are events, not state”Components never change the data you pass in (msa, tree). When the user renames or removes a
row, the component calls onRenameRow / onRemoveRows / onRemoveColumns, and you apply the
change and pass the new data back. That keeps edit history, undo and validation in your hands —
see Editing an alignment.