State
This article describes the state API.
You can create a state by passing a design object to the createState
function—or, in a React project, the createStateDesigner
hook. Once you’ve created a state, you can use and interact with it using the values and methods described below.
What is a State?
A state
manages information about the state of a user interface. It stores the current version of that information and controls when and how it can change. When the information does change, a state
can notify other parts of your project so that everything stays in sync.
The core of the state object is the snapshot of the state’s current information:
It also contains several helpers to interpret that information:
It also contains a function that you can use to send events to the state:
And a function to subscribe to its changes:
data
The data
property corresponds to the design’s data
property, except that state.data
will always store the most recent version of the data
.
CODE
When an event occurs that changes the state’s data
, the data
property will store the latest version.
CODE
The data
value is immutable: it will be new after each change. Its properties will only be new if those properties have changed during the update.
CODE
values
The values
property contains the state’s current computed values. This object corresponds to the design object’s values
object, except that the state.values
object contains the results of the design’s functions.
CODE
Whenever the state updates, it will re-compute its values
by running each function again. Like data
, this object will be new after each update.
CODE
isIn
The isIn
function will return true
if the indicated state is currently active.
const state = createState({initial: "high",states: {high: {on: { TOGGLE: { to: "low" } },},low: {on: { TOGGLE: { to: "high" } },},},})state.isIn("high") // truestate.send("TOGGLE")state.isIn("high") // false
Using Paths
You can indicate a state node with either its name or its path.
state.isIn("active")state.isIn("bold.active")state.isIn("text.bold.active")
Tip: Technically, the state’s name is a path with a depth of only one. A state’s path is made up of its parent states, separated by periods. A full path might look something like
#state_id.root.text.bold.active
.Indicating states with paths is useful when your design includes multiple states that share the same name, such as
bold.active
anditalic.active
.
Testing Multiple States
You can also use the isIn
method to test if more than one state is active. When testing multiple states, the method will return true
only if all of the indicated states are active.
const state = createState({initial: "high",states: {high: {},low: {},med: {},},})state.isIn("low", "med") // falsestate.isIn("high", "low") // falsestate.isIn("high", "root") // true
isInAny
The isInAny
method works exactly like isIn
except that when testing multiple states it will return true
if any of the indicated states are active.
const state = createState({initial: "high",states: {high: {},low: {},med: {},},})state.isIn("low", "med") // falsestate.isIn("high", "low") // true// highlight-next-linestate.isIn("high", "root") // true
whenIn
The whenIn
helper method allows you to return different values depending on which states are active.
buttonText = state.whenIn({stopped: "Play",playing: "Stop",}) // "Play"state.send("PLAYED") // Transition to `playing`buttonText = state.whenIn({stopped: "Play",playing: "Stop",}) // "Stop"
Using Paths
As with isIn
, you can indicate a state using a path of any depth.
state.whenIn({heading: "H1","text.subheading": "H2","editing.text.body": "Body",})
Output
By default, the whenIn
helper will return a value. If multiple indicated states are active, then this value will be the last active state indicated.
state.whenIn({asleep: "Asleep",awake: "Awake", // Activeworking: "Working", // Active}) // "Working"
You can use the method’s second argument to produce different results. This argument is "value"
by default, but you can also set it to "array"
.
state.whenIn({asleep: "Asleep",awake: "Awake", // Activeworking: "Working", // Active},"array") // ["Awake", "Working"]
You can also set it to a reducer function. In this case, you can provide the reducer’s initial value as the function’s third argument.
state.whenIn({asleep: "Asleep",awake: "Awake", // Activeworking: "Working", // Active},(acc, cur) => acc + " and " + cur,"I'm ") // "I'm Awake and Working"
Tip: In React, you can use the
whenIn
helper to return different JSX depending on the currently active state(s). When using the"array"
format, remember to add a unqiuekey
property to each element.<ul>{whenIn({asleep: <li key="0">Asleep</li>,awake: <li key="1">Awake</li>,working: <li key="2">Working</li>,})}</ul>
active
The active
property contains an array of paths for all active states.
const state = createState({initial: "high",states: {high: {on: { TOGGLE: { to: "low" } },},low: {on: { TOGGLE: { to: "high" } },},},})state.active // ["#state_id.root", "#state.id_root.high"]
stateTree
The stateTree
property includes the full tree of all state nodes.
const state = createState({initial: "high",states: {high: {on: { TOGGLE: { to: "low" } },},low: {on: { TOGGLE: { to: "high" } },},},})// Print the entire state treeconsole.log(JSON.stringify(state.stateTree,(k, v) => (typeof v === "function" ? v.name : v),2))
Tip: For now, at least, this object is mutable—but for heaven’s sake, don’t mutate it! Use it instead for debugging, visualization, and to better understand the relationship between a state design and the resulting state tree.
onUpdate
You can subscribe to a state’s changes by passing a callback function to the state’s onUpdate
method. When a state changes, it will call each subscribed callback with itself as the only argument.
Note: The state passed to the callback is the same as (or strictly equal to) the state that was originally subscribed to. Its methods, such as
send
will also be the same. However, the state will have newdata
andactive
properties following each update.const state = createState(myDesign)const { data, active, send } = statestate.onUpdate((update) => {console.log(update === state) // true// Compared with state valuesconsole.log("data", update.data === state.data) // trueconsole.log("active", update.active === state.active) // trueconsole.log("send", update.send === state.send) // true// Compared with destructured valuesconsole.log("data", update.data === data) // falseconsole.log("active", update.active === active) // falseconsole.log("send", update.send === send) // true})
The onUpdate
method returns a second function that will cancel the subscription.
const state = createState(myDesign)cancel = state.onUpdate((update) => {// Do something})cancel()
Tip: You could use this to cleanup a subscription that you’ve made in an effect:
const state = createState(myDesign)function Example() {const [state, setState] = React.useState()React.useEffect(() => {return state.onUpdate((update) => setState(update.data))}, [])return <div>...</div>}The
useStateDesigner
hook takes care of this for you, though!