React Patterns
At Stately, we love this combo. It’s our go-to stack for creating internal applications.
To ask for help, check out the #react-help
channel in our Discord community.
Local state​
Using React hooks are the easiest way to use state machines in your components. You can use the official @xstate/react
to give you useful hooks out of the box, such as useMachine
.
import { useMachine } from '@xstate/react';
import { toggleMachine } from '../path/to/toggleMachine';
function Toggle() {
const [current, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{current.matches('inactive') ? 'Off' : 'On'}
</button>
);
}
Global State/React Context​
Our recommended approach for managing global state with XState and React is to use React Context.
There are two versions of 'context': XState’s context and React’s context. It’s a little confusing!
Context Provider​
React context can be a tricky tool to work with - if you pass in values which change too often, it can result in re-renders all the way down the tree. That means we need to pass in values which change as little as possible.
Luckily, XState gives us a first-class way to do that: useInterpret
.
import React, { createContext } from 'react';
import { useInterpret } from '@xstate/react';
import { authMachine } from './authMachine';
export const AuthActorContext = createContext({});
export const GlobalStateProvider = (props) => {
const authActor = useInterpret(authMachine);
return (
<AuthActorContext.Provider value={authActor}>
{props.children}
</AuthActorContext.Provider>
);
};
Using useInterpret
returns an actor, which is a static reference to the running machine which can be subscribed to. This value never changes, so we don't need to worry about wasted re-renders.
For Typescript, you can create the context as
createContext({} as InterpreterFrom<typeof authMachine>);
to ensure strong typings.
Utilizing context​
Further down the tree, you can subscribe to the actor like this:
import React, { useContext } from 'react';
import { AuthActorContext } from './globalState';
import { useActor } from '@xstate/react';
export const SomeComponent = (props) => {
const authActor = useContext(AuthActorContext);
const [state] = useActor(authActor);
return state.matches('loggedIn') ? 'Logged In' : 'Logged Out';
};
The useActor
hook listens for whenever the actor changes, and updates the state value.
Improving Performance​
There's an issue with the implementation above - this will update the component for any change to the actor. Tools like Redux use selectors
for deriving state. Selectors are functions which restrict which parts of the state can result in components re-rendering.
Fortunately, XState exposes the useSelector
hook.
import React, { useContext } from 'react';
import { AuthActorContext } from './globalState';
import { useSelector } from '@xstate/react';
const loggedInSelector = (state) => {
return state.matches('loggedIn');
};
export const SomeComponent = (props) => {
const authActor = useContext(AuthActorContext);
const isLoggedIn = useSelector(authActor, loggedInSelector);
return isLoggedIn ? 'Logged In' : 'Logged Out';
};
If you need to send an event in the component that consumes an actor, you can use the actor.send(...)
method directly:
import React, { useContext } from 'react';
import { AuthActorContext } from './globalState';
import { useSelector } from '@xstate/react';
const loggedInSelector = (state) => {
return state.matches('loggedIn');
};
export const SomeComponent = (props) => {
const authActor = useContext(AuthActorContext);
const isLoggedIn = useSelector(authActor, loggedInSelector);
// Get `send()` method from an actor
const { send } = authActor;
return (
<>
{isLoggedIn && (
<button type="button" onClick={() => send('LOG_OUT')}>
Logout
</button>
)}
</>
);
};
This component will only re-render when state.matches('loggedIn')
returns a different value. This is our recommended approach over useActor
for when you want to optimise performance.
Dispatching events​
For dispatching events to the global store, you can call an actor's send
function directly.
import React, { useContext } from 'react';
import { AuthActorContext } from './globalState';
export const SomeComponent = (props) => {
const authActor = useContext(AuthActorContext);
return <button onClick={() => authActor.send('LOG_OUT')}>Log Out</button>;
};
Note that you don’t need to call useActor
for this, it’s available right on the context.
Other hooks​
XState’s useMachine
and useInterpret
hooks can be used alongside others. Two patterns are most common:
Named actions/actors/guards​
Let’s imagine that when you navigate to a certain state, you want to leave the page and go somewhere else, via react-router
or next
. For now, we’ll declare that action as a ‘named’ action - where we name it now and declare it later.
import { createMachine } from 'xstate';
export const machine = createMachine({
initial: 'toggledOff',
states: {
toggledOff: {
on: {
TOGGLE: 'toggledOn',
},
},
toggledOn: {
entry: ['goToOtherPage'],
},
},
});
Inside your component, you can now implement the named action. I've added useHistory
from react-router
as an example, but you can imagine this working with any hook or prop-based router.
import { machine } from './machine';
import { useMachine } from '@xstate/react';
import { useHistory } from 'react-router';
const Component = () => {
const history = useHistory();
const [state, send] = useMachine(machine, {
actions: {
goToOtherPage: () => {
history.push('/other-page');
},
},
});
return null;
};
This also works for actors, guards, and delays.
If you use this technique, any references you use inside
goToOtherPage
will be kept up to date each render. That means you don’t need to worry about stale references.
Syncing data with useEffect​
Sometimes, you want to outsource some functionality to another hook. This is especially common with data fetching hooks such as react-query
and swr
. You don’t want to have to re-build all your data fetching functionality in XState.
The best way to manage this is via useEffect
.
const Component = () => {
const { data, error } = useSWR('/api/user', fetcher);
const [state, send] = useMachine(machine);
useEffect(() => {
send({
type: 'DATA_CHANGED',
data,
error,
});
}, [data, error, send]);
};
This will send a DATA_CHANGED
event whenever the result from useSWR
changes, allowing you to react to it just like any other event. You could, for instance:
- Move into an
errored
state when the data returns an error - Save the data to context
Class components​
If you’re using class components, here's an example implementation that doesn’t rely on hooks.
- The
machine
is interpreted and itsactor
instance is placed on the component instance. - For local state,
this.state.current
will hold the current machine state. You can use a property name other than.current
. - When the component is mounted, the
actor
is started viathis.actor.start()
. - When the component will unmount, the
actor
is stopped viathis.actor.stop()
. - Events are sent to the
actor
viathis.actor.send(event)
.
import React from 'react';
import { interpret } from 'xstate';
import { toggleMachine } from '../path/to/toggleMachine';
class Toggle extends React.Component {
state = {
current: toggleMachine.initialState,
};
actor = interpret(toggleMachine).onTransition((current) =>
this.setState({ current }),
);
componentDidMount() {
this.actor.start();
}
componentWillUnmount() {
this.actor.stop();
}
render() {
const { current } = this.state;
const { send } = this.actor;
return (
<button onClick={() => send('TOGGLE')}>
{current.matches('inactive') ? 'Off' : 'On'}
</button>
);
}
}