• Introduction
  • Designing State
  • Quick Start

API

Usage

Examples

States

A design object supports a tree of states nodes (or “states”) that can be either active or inactive. These states nodes serve two purposes: together with data, they represent the state of the user interface through which states are active and inactive; and, because events may only be handled on active states, they provide a way to handle the same event differently under certain circumstances.

The design object describes the root state node. Like any other state, this root state may have child states—and these states may have child states of their own, and so on.

All states support the same set of properties.

states

A state’s states property is an object containing the state’s child states.

const state = createState({
states: {
land: {},
sea: {},
},
})

States are a repeating pattern: the root state may have child states, and each of those child states may have their own child states, and so on to any depth.

const state = createState({
states: {
land: {
states: {
walking: {},
running: {},
},
},
sea: {
states: {
swimming: {},
diving: {},
},
},
},
})

initial

A state’s initial property serves two purposes: it identifies this state as a branch state, where only one of its child states may be active at a time, and it indicates which child state should be the “initially active” state. When the parent state becomes active, this initial child state will also become active, while all other child states will be inactive.

[
  "#body.root",
  "#body.root.standing"
]
PREVIEW
CODE

In the example above, the root state is a branch state, meaning that one only of its child states will be active at a time.

If the initial property is not provided, then the state will be considered a parallel state. In a parallel state, all child states will be active simultaneously whenever the parent state is active.

[
  "#body.root",
  "#body.root.attitude",
  "#body.root.attitude.happy",
  "#body.root.pose",
  "#body.root.pose.standing"
]
PREVIEW
CODE

In the example above, the root state is a parallel state, meaning that both the attitude and pose states will be active at the same time. However, each of these states are branch states, and will only have one active child state each.

Tip: If we didn’t have parallel states, we would need to create separate states for happyAndStanding, sadAndStanding, happyAndSitting, and so on. As you can imagine, it wouldn’t take much for this to become a major problem!

on

A state’s on property is an object that defines which events the state can handle, as well as how the event should handle those events. These events are stored as properties of the on object, with the property’s key being the event’s name and one or more event handlers as the property’s value.

const state = createState({
on: {
WELCOMED: (data) => (data.message = "Hello world!"),
},
})

Events in the on object describe things that can happen outside of your state and that, when they occur, should produce some effect inside of it. As far as your design is concerned, an event will “occur” when it is sent to the state using the state’s send method.

const state = createState({
data: {
message: "...",
},
on: {
BORN: (data) => (data.message = "Hello world!"),
},
})
state.data.message // "..."
state.send("BORN")
state.data.message // "Hello world!"

There are two types of changes that an event may produce: a change to the state’s data through an action, as shown in the example above, or a change to the state’s active and inactive state nodes though a transition.

const state = createState({
initial: "low",
states: {
high: {},
low: {},
},
on: {
SET_HIGH: { to: "high" },
},
})
state.isIn("high") // false
state.send("SET_HIGH")
state.isIn("high") // true

In your design, you can also define events that should happen inside of your state, either automatically or as the result of some other event.

onEnter

If a state has an onEnter event, then that event will be handled whenever the state changes from inactive to active as the result of a transition.

const state = createState({
data: { temperature: 18 },
initial: "low",
states: {
high: {
onEnter: (data) => (data.temperature = 30),
},
low: {},
},
on: {
SET_HIGH: { to: "high" },
},
})
state.data // { temperature: 18 }
state.send("SET_HIGH")
return log(state.data) // { temperature: 30 }

An onEnter will also occur when a state is first created. (One of the last things that the createState function does is to switch the root state from inactive to active.)

const state = createState({
data: { count: 0 },
onEnter: () => data.count++,
})
state.data // { count: 1 }

Note: Because an onEnter event’s handler may produce its own transition, it’s possible to create a design that bounces indefinitely between two states.

const state = createState({
initial: "low",
states: {
high: {
onEnter: { to: "low" },
},
low: {
onEnter: { to: "high" },
},
},
on: {
SET_HIGH: { to: "high" },
},
})

A state will throw an error if it detects an infinite loop like this at runtime. Best to take care when using an transition in an internal event like onEnter.

onExit

A state’s onExit event will be handled whenever the state changes from active to inactive as the result of a transition.

const state = createState({
data: { exits: 0 },
initial: "atHome",
states: {
atHome: {
onExit: (data) => data.exits++,
},
outside: {},
},
on: {
LEFT_HOUSE: { to: "outside" },
},
})
state.data // { exits: 0 }
state.send("LEFT_HOUSE")
return log(state.data) // { exits: 1 }

Note: An onExit event defined at the root of a design object will never run because a state’s root state node will never become inactive.

onEvent

A state’s onEvent event will be handled whenever the state receives an event through its send method while that state is active. The event does not need to be handled elsewhere in order for the onEvent handler to run.

const state = createState({
data: { events: 0 },
onEvent: (data) => data.events++,
})
state.data // { events: 0 }
state.send("RAISED")
state.send("LOWERED")
state.data // { events: 2 }

repeat

A state’s repeat property allows you to define an event, onRepeat, that will be handled on an interval while its state is active.

const state = createState({
data: { seconds: 0 },
repeat: {
onRepeat: (data) => data.seconds++,
delay: 1,
},
})
state.data // { seconds: 0 }
await twoSecondPause()
state.data // { seconds: 2 }

The delay property is optional: if you leave it out, the event will be handled on every animation frame, or roughly sixty times per second.

0

PREVIEW
CODE

Tip: If a design includes multiple event handlers that repeat on each frame, then these event handlers will be batched so that they produce at most one update per frame.

Updating a React component on every animation frame can lead to performance issues. The useStateDesigner hook updates each time the state notifies its subscribers, and a state notifies its subscribers any time that an action or transition occurs. However, you can use the secretlyDo and secretlyTo event handler properties to run actions and transition without triggering a notification—which will, in turn, prevent it from causing any component updates via useStateDesigner.

In the example below, the count will still update on each frame but, unlike the example above, the component will only update when starting or stopping the timer.

0

PREVIEW
CODE

async

An async event allows you to make an asynchronous request and then handle the request differently depending on its outcome.

const url = "https://dog.ceo/api/breeds/image/random"
const state = createState({
data: { events: 0 },
async: {
await: async function () {
const response = await fetch(url)
return response.json()
},
onResolve: (data, _, result) => {
data.message = result.message
},
onReject: () => {
data.message = "Result failed!"
},
},
})

The async property is an object with three properties: await, onResolve, and onReject.

The await property accepts either an async function or a function that returns a promise. When the state becomes active, it will wait for this promise to either resolve or reject. If it resolves, then the state will run its onResolve event; otherwise, it will run its onReject event. Both event handlers will receive the resolved (or rejected) data as its initial result.

See the this project for a more complete example.