statekit • Page 7 of 14
Data and events
17/10/2026 • 3 min read
data is what the machine holds. events change it.
The draft
An event gets a draft of the current data, an
Immer draft. Change it like a normal
object, and when the event ends the changes become the next snapshot. The
draft is one field of the event's first parameter, which also has state,
setState, S and hasState for states, and the
machine's id.
import { createMachine } from "@statekit/core"
const cartMachine = createMachine({
data: {
items: [] as { sku: string; quantity: number }[],
},
events: {
add: ({ draft }, sku: string) => {
const item = draft.items.find(
(item) => item.sku === sku
)
if (item) item.quantity++
else draft.items.push({ sku, quantity: 1 })
},
clear: ({ draft }) => {
draft.items = []
},
},
})
What you didn't touch keeps its identity: after add, cartMachine.data.items
is a new array, but the unchanged items are the same objects.
An event can return a value, and calling it returns that value. Don't keep the draft past the event.
Async events
An async event is an async function. Changes made before its first await
are published right away, and changes made after its last await when it
settles:
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))
const noteMachine = createMachine({
states: ["idle", "saving", "saved", "failed"],
data: { message: "Not saved yet", saves: 0 },
events: {
save: async (
{ draft, setState, S },
fail: boolean
) => {
setState(S.saving)
draft.message = "Saving…"
try {
await sleep(1500)
if (fail)
throw new Error("The network is down")
draft.saves++
draft.message = `Saved ${draft.saves} times`
setState(S.saved)
} catch (error) {
draft.message = (error as Error).message
setState(S.failed)
throw error
}
},
},
})
The log under the demo is subscribeEvents, which
reports when an event starts and how it ends:
Loading demo…
Calling the event returns its promise. It resolves to what the event returns and rejects with what it throws:
await noteMachine.event.save(false)
noteMachine.data.message // "Saved 1 times"
No writes between two awaits
Write to the draft before the first await or after the last. A write between
two awaits is published when the event ends, or earlier if anything else on
the machine publishes first, so you can't tell when it will show:
load: async ({ draft }, id: number) => {
// ❌ another await follows
draft.user = await fetchUser(id)
// ✅ after the last await
draft.posts = await fetchPosts(id)
}
When a middle step should show, give it its own event.
@statekit/eslint-plugin reports these writes.
setState between two awaits behaves the same: state changes at once, but
components hear about it when the event next publishes.
Parallel events
Async events on one machine share one draft, so none overwrites another's changes. The flip side is that when one publishes, whatever the others have written so far goes out with it.
When an event throws
The changes made before the throw stay, with no rollback. The event reports
false to subscribeEvents and throws to its caller. In the demo, "Save, then
fail" ends with the error message in data and a rejected promise.
Outside events
Data is deeply frozen, including the object you passed as data. Writing to it
or sorting it in place throws a TypeError:
cartMachine.data.items.push(item) // throws
cartMachine.data.items.sort() // throws
const sorted = [...cartMachine.data.items].sort() // fine
setData is for syncing: when two different sources represent one thing, it
copies one into the machine without going through an event. Give it a function
that gets a draft:
cartMachine.setData((draft) => {
draft.items = savedCart.items
})
It isn't reported to subscribeEvents and has no name to search for, so use an
event for anything else.
#javascript, #npm, #react, #readme, #typescript, #web-development