Sometimes a controller dispatches an action and needs to wait until the machine reaches a meaningful resting point: a particular state, an output action, or some predicate over either.
Reach for waitUntil when:
- You dispatch an action and want a single
Promisethat resolves when the machine settles into a specific state. - You need to map several possible output actions onto one outcome
(
Closed → true,Blocked → false). - You want to await a condition that may already be true when you start
listening (e.g. "wait until we're in
Ready"). - You want cancellation: an
AbortSignalortimeoutcleans up the underlying subscriptions for you.
For "subscribe to every transition" or "render the current state", keep
using runtime.onContextChange, runtime.onOutput, and the React hooks
in react-integration.md.
There are two pieces:
- A matcher describes the condition.
runtime.waitUntil(and its sugar) subscribes, races the matcher against the optionalsignal/timeout, and returns a Promise.
import { matchOutput, matchState } from "@tdreyno/fizz"
// Wait for a state.
const ready = await runtime.waitUntilState(States.Ready)
// Wait for the next output of type "Saved".
const saved = await runtime.waitUntilOutput(savedAction)
// Map outputs to outcomes (primitive shorthand).
const outcome = await runtime.waitUntilOutput({
Closed: true,
Blocked: false,
})
// Or pass a function per type for derived values.
const derived = await runtime.waitUntilOutput({
Saved: action => action.payload,
})waitUntilState accepts either a state constructor or a matchState
result. waitUntilOutput accepts an action creator, a handler map keyed
by action type, a predicate function, or a matchOutput result.
Handler-map entries can be either a function (action) => value | undefined
or a direct value. Direct values are returned as the wait result whenever
the action type matches, which is handy for predicate-style mappings like
{ Saved: true, Failed: false }. Function entries that return
undefined are treated as "no match" and let the wait keep listening.
matchState takes a where predicate over the state's data so you can
wait for a particular shape:
import { matchState } from "@tdreyno/fizz"
await runtime.waitUntilState(
matchState(States.Loaded, { where: data => data.ready }),
)matchAny runs against both state transitions and outputs:
import { matchAny } from "@tdreyno/fizz"
const settled = await runtime.waitUntil(
matchAny(event => {
if (event.kind === "state" && event.state.is(States.Ready)) {
return "ready" as const
}
if (event.kind === "output" && event.output.type === "Error") {
return "error" as const
}
return undefined
}),
)runUntil is sugar for "subscribe to a matcher, then dispatch an action,
then await the matcher". It avoids the race where a synchronous
transition would fire before the subscriber is attached:
const ready = await runtime.runUntil(start(), matchState(States.Ready))All wait helpers accept the same options object:
type WaitUntilOptions = {
signal?: AbortSignal
timeout?: number
includeCurrent?: boolean
}signalrejects the wait withWaitUntilAbortErrorwhen aborted.timeout(milliseconds) rejects withWaitUntilTimeoutError.includeCurrentdefaults totruefor state matchers; the wait resolves immediately (via microtask) if the current state already matches. Set tofalseto require an explicit transition.- If the runtime disconnects while a wait is pending, the Promise
rejects with
RuntimeDisconnectedError.
const controller = new AbortController()
const promise = runtime.waitUntilOutput(savedAction, {
signal: controller.signal,
timeout: 5_000,
})
controller.abort() // promise rejects with WaitUntilAbortError@tdreyno/fizz-react ships matching hooks. They take the runtime from a
useMachine() call (or any other source) and abort on unmount:
import { matchState, useMachine } from "@tdreyno/fizz-react"
import { useRunUntil, useWaitUntilState } from "@tdreyno/fizz-react"
function ReadyBadge() {
const machine = useMachine(MyMachine, MyMachine.states.Initializing({}))
const ready = useWaitUntilState(machine.runtime, MyMachine.states.Ready)
if (ready.status === "pending") return <span>Loading…</span>
if (ready.status === "rejected") return <span>Error</span>
return <span>Ready</span>
}
function Save() {
const machine = useMachine(MyMachine, MyMachine.states.Editing({}))
const runUntil = useRunUntil(machine.runtime)
return (
<button
onClick={async () => {
await runUntil(save(), matchState(MyMachine.states.Saved))
}}
>
Save
</button>
)
}useRunUntil aborts the previous wait when the callback is called again
and on unmount.
- Async: for the underlying scheduling model.
- Testing:
runUntilis a good fit for tests that need a single Promise for a round-trip. - Dispatch And Read: for read-after-dispatch patterns that don't need awaiting.
- React Integration: for the hook surface.