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.
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.
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") // falsestate.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.
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.
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.