State & actions
Props describe how a component looks when it mounts. Some components also do things — a toast fires, a card reacts to a pointer, a menu opens. windo gives a windo a small slice of component-local state and a set of actions that drive it, so you can rehearse those behaviours in the canvas instead of only their resting states.
The state object
A windo can carry a state object: an initial bag of key/value pairs that lives across renders and is editable from outside the component. It's optional — windos that only document a resting prop set don't need it. But the moment a component has behaviour worth showing — a counter, an open flag, a "fired" tick — that behaviour belongs in state, not in the props.
The distinction matters. Props are the JSON-editable surface validated by your schema; they answer what does this look like. State answers what is it doing right now. A toast's variant and title are props; the fact that one is currently on screen is state.
state: { tick: 0 }One generic, three jobs
state is typed by the second generic on the factory: windo<Props, State>(...). You write it once, and that single type flows everywhere state is touched.
export default windo<ToastProps, { tick: number }>(() => ({ // ... state: { tick: 0 }, }))
That one annotation is deliberate. There are three places state shows up at render time, and a single explicit generic drives all of them:
- the shape of
stateitself, - the value you read back as
ctx.state, - the patch you hand to
ctx.setState.
Because the type lives on the factory, none of those need a per-call generic. ctx.state.tick is a number; ctx.setState({ tick: 1 }) type-checks; ctx.setState({ tikc: 1 }) does not. The compiler keeps the initial value, the reads, and the writes in lockstep from one source.
Reading and writing through ctx
Every render-time function on a windo receives the live WindoRenderContext as its second argument — component, each action's run, and so on. Two of its fields are the state channel:
ctx.state— the current state, typed as yourState.ctx.setState(patch)— merge aPartial<State>into it. The merge re-renders the preview, and the chrome echoes the new state as a read-only strip so you can see exactly what the component is reacting to.
setState takes a partial patch, not a full replacement — pass only the keys that changed and the rest are preserved. Here the component reads ctx.state to decide what it renders:
component: (props, ctx) => ( <Card {...props} eyebrow={ctx.state.hovered ? 'Hovering ✦' : props.eyebrow} /> )
Actions
Reads happen inside the component. Writes happen from actions — out-of-band entry points that mutate state without the user touching the prop editor. A windo's actions array holds them, and each action is a small object:
| Name | Type | Description |
|---|---|---|
| label | string | Display name — used for a toolbar button, and as the action's identity. |
| on | WindoActionTrigger | How the action fires. Defaults to "click". |
| run | (ctx, active) => void | The effect. Receives the live ctx and an active flag. |
| disabled | (ctx) => boolean | Optional predicate; greys out a click action's button. |
Toast's two actions are the canonical shape. One bumps the tick — the component turns each new tick into a fired toast — and the other reaches past windo entirely to clear Sonner's stack. run is free to do whatever a render-time function can: call setState, fire an imperative API, log.
actions: [ { label: 'Show toast', run: c => c.setState({ tick: c.state.tick + 1 }) }, { label: 'Dismiss all', run: () => toast.dismiss() }, ]
The four triggers
on is one of four values — the set is exported as WINDO_ACTION_TRIGGERS. They split into two families: a toolbar button, or a binding to the preview stage's pointer events.
| Trigger | Fires on | active flag |
|---|---|---|
| click | A toolbar button you press (the default). | always true |
| enter | Pointer enters the preview stage. | always true |
| exit | Pointer leaves the preview stage. | always true |
| hover | Either edge of a stage hover. | true on enter, false on leave |
click is the one that renders UI: it adds a button to the canvas toolbar, and pressing it runs the action. The other three bind to pointer events over the stage and fire on their own — there's no button.
hover is the interesting one. Where enter and exit are two separate one-shot actions, hover is a single action that runs on both edges, distinguished by the second argument to run. That active flag is true on pointer-enter and false on pointer-leave — so one action expresses an entire hover state. (For click, enter, and exit, active is always true.)
Card uses exactly this to mirror a real hover state into the canvas:
state: { hovered: false },
actions: [
{ label: 'Hover', on: 'hover', run: (c, active) => c.setState({ hovered: active }) },
],
// the component then reads ctx.state.hoveredDisabling an action
A click action can carry a disabled predicate. It's a function of the live ctx, re-evaluated as state changes, and when it returns true the toolbar button greys out. Use it to gate an action on the current state — a "Dismiss" that only lights up once something has been shown:
actions: [ { label: 'Show toast', run: c => c.setState({ tick: c.state.tick + 1 }) }, { label: 'Dismiss all', run: () => toast.dismiss(), disabled: c => c.state.tick === 0, }, ]
Logging to the Console
When an action does something you can't see in the rendered output — a network call, a value you want to inspect — write it to the chrome's Console tab with ctx.logger.log(...). It takes the same variadic arguments as console.log, and each call posts one entry to the Console strip:
actions: [
{
label: 'Show toast',
run: c => {
c.logger.log('fired toast', c.state.tick + 1)
c.setState({ tick: c.state.tick + 1 })
},
},
]That keeps action behaviour observable without reaching for the browser devtools — the log lands in the same chrome that shows the state strip, right next to the component it describes.
Where next
- Configurable props — the JSON-editable, zod-validated half of a windo, and how it differs from state.
- Contexts — ambient values and providers that the same
ctxexposes alongside state.