After having created an abstract state machine that models the behavior of our application, it is now time to wire the machine to a React component. Our invoked state machine (also known as service) should dictate the current state of our component at all times.
If you want to jump right into the CodeSandbox, feel free to do so. Below you can find all the relevant pieces of code with a brief explanation.
Here is the light switch machine we've created again.
import { Machine } from 'xstate';
const lightSwitchMachine = Machine({
id: 'lightSwitch',
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: 'active'
}
},
active: {
on: {
TOGGLE: 'inactive'
}
},
}
});
If we want to use the machine within a React component, we need to install a tiny package called @xstate/react
. It exports a couple of hooks with which we can invoke state machines and friends.
import React from 'react';
import { useMachine } from '@xstate/react';
import Switch from 'react-switch';
const LightSwitch = () => {
const [state, send] = useMachine(lightSwitchMachine);
return (
<Switch
onChange={() => send({ type: 'TOGGLE' })}
checked={state.matches('active')}
aria-label='Toggle me'
/>
);
};
Don't mind the imported <Switch />
component. We are merely using it so that we don't have to write custom CSS to get a decent looking switch.
Instead, I want to focus on the useMachine
hook. You can find its full API specification here. It takes in our machine as an argument and returns an array. The first entry is the state
object of our invoked machine. It is not a string value telling us whether the service is in the active
or inactive
state. It's an object with a lot of properties to access the internals of our invoked state machine. Very rarely, we'll be interested in anything other than state.matches
.
As an aside, if you don't want to worry about the other properties, you could totally just destructure the one you'll need.
// array destructering, object destructering and aliasing to isStateMatching
const [{matches: isStateMatching}, send] = useMachine(lightSwitchMachine);
// then use it like this
isStateMatching('active')
Going back to our example from above, matches()
is a function that returns a boolean. Use it to assert whether the current state of our service, matches the state node passed as a first argument.
checked={state.matches('active')}
In plain English, this line is saying: If our invoked machine is in the active state, pass true
for the checked
prop of our Switch
component. If our machine is in any other state, pass false
.
The last thing that we need to cover for today is the send
function. If you've used the useReducer
hook or Redux in the past, you might be used to calling this function dispatch
. Even though you should be able to ditch Redux at the end of the series, you can continue to use the name dispatch
if it works for you.
As the name indicates, it is used to send events to the invoked machine which can react to the event by transitioning from one state to another. There are two ways to send events.
You can either pass an object to the send
function and set the event name as a value of the type
key. This is how I used it in the example above send({type: 'TOGGLE'})
, or you can just pass the event name as a string value send('TOGGLE')
.
I hope this post gave you a brief overview on how to invoke an abstract machine and use the service within a React component. I'm very excited about the post tomorrow as we'll solidify our learnings from today by looking at the differences between an implicit and explicit state machine.
About this series
Throughout the first 24 days of December, I'll publish a small blog post each day teaching you about the ins and outs of state machines and statecharts.
The first couple of days will be spent on the fundamentals before we'll progress to more advanced concepts.
Top comments (2)
Thank you for writing this, I'm enjoying reading those small chunks.
But in this post I was surprised: For simplicity and less future code changes I would suggest to not rename the destrctured
matches
function.Also everybody knowing the API of StateX will recognize the method and know what the code means, without checking where this custom name comes from.
In a simple example like yours it's of course not such a big issue, but if this becomes a habit or the code lives (and grows) longer then expected, as we all know it does most of the time, keeping it simple helps.
Thx again for and keep up the good writing!
Yes you are right. I'd never destructure it in my code personally but thought I'd mention it anyway as it might be easier for some beginners to just deal with the one important function than to use the
state
object and possibly be confused by all the other stuff that they'll see in their editor once they typestate.
Thank you for your feedback :)