How XState's final states work, what done() events are, and the pattern for composing machines that signal completion to a parent.
XState has a concept called final states — states that, when entered, signal the machine is done. This matters most when composing machines: a parent machine can wait for a child machine to finish.
import { createMachine } from "xstate";
const checkoutMachine = createMachine({
id: "checkout",
initial: "cart",
states: {
cart: {
on: { PROCEED: "payment" }
},
payment: {
on: {
SUCCESS: "confirmed",
FAIL: "failed"
}
},
confirmed: { type: "final" }, // machine is done
failed: { type: "final" }, // machine is done (with error)
}
});When the machine enters confirmed or failed, it emits a done event automatically.
Karanveer Singh Shaktawat
Full Stack Engineer & Infrastructure Architect
Building portfolio, contributing to open source, and seeking remote full-time roles with significant technical ownership.
Pick what you want to hear about — I'll only email when it's worth it.
Did this resonate?
A common Next.js App Router misconception: 'use client' doesn't make a file client-only — it marks the boundary where the client tree starts.
The three ways the frontend talks to the Rust backend in Tauri, when to use each, and the one gotcha that wasted an afternoon.
const orderMachine = createMachine({
id: "order",
initial: "processing",
states: {
processing: {
invoke: {
src: checkoutMachine,
onDone: {
target: "complete",
actions: (ctx, event) => console.log("checkout done:", event.data)
},
onError: "error"
}
},
complete: { type: "final" },
error: {}
}
});onDone fires when the invoked machine reaches any final state. event.data contains the context of the child machine at the time it finished.
I used this in a hotel management system I built. The reservation machine had final states for confirmed, cancelled, and no_show. The main booking flow invoked the reservation machine and used onDone to trigger the next step (invoice, room assignment, etc.) only after the reservation machine had definitively resolved.
Without final states, you'd use flags in shared context — which is just state management debt waiting to happen. Final states make termination explicit and composable.
A parallel state (multiple regions running simultaneously) finishes when all its regions enter a final state. This is useful for "wait for all validation steps to complete" patterns — each validation step is a region, and the parent waits for all of them.