Skip to content
Version: XState v4

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 its actor 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 via this.actor.start().
  • When the component will unmount, the actor is stopped via this.actor.stop().
  • Events are sent to the actor via this.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>
);
}
}