Skip to content
5 minute read

Global state with XState and React

David Khourshid

XState is a versatile state management & orchestration library that works with any framework, including React with the @xstate/react package. For many apps, managing global state is a requirement, and there are many options for sharing global state in React, like using React Context or libraries like Redux, MobX, and Zustand.

The @xstate/react package makes it simple to manage component-level state with hooks like useMachine() and useActor(), but it works equally well for managing global state 🌎

Quick start

  1. Create the global logic. This can be as simple as a promise or function, or as complex as a state machine or statechart.
  2. Create an actor from that logic and export it.
  3. Import that actor from any component and:
    • use the useSelector(…) hook to read the actor's snapshot
    • call actorRef.send(…) to send events to it.

That's it!

import { setup, createActor } from 'xstate';
import { useSelector } from '@xstate/react';

// Your global app logic
import { globalLogic } from './globalLogic';

// Create the actor
export const globalActor = createActor(globalLogic);
// Start the actor
globalActor.start();

export function App() {
// Read the actor's snapshot
const user = useSelector(globalActor, (snapshot) => snapshot.context.user);

return (
<div>
<h1>Hello, {user.name}!</h1>
// Send events to the actor
<button onClick={() => globalActor.send({ type: 'logout' })}>
Logout
</button>
</div>
);
}

Global state

The simplest way to manage global state is to share an actor instance between components. Think of an actor as a "store" – you can subscribe to its state updates ("snapshots") and send events to it.

You can use that actor in any component either by passing it as a prop or referencing it directly from module scope.

import { setup, createActor } from 'xstate';
import { useSelector } from '@xstate/react';

// Global app logic
const countMachine = createMachine({
context: { count: 0 },
on: {
inc: {
actions: assign({ count: ({ context }) => context.count + 1 })
}
}
});

// Global actor - an instance of the app logic
export const countActor = createActor(countMachine);
countActor.start(); // Starts the actor immediately

export function App() {
// Read the actor's snapshot
const count = useSelector(countActor, (state) => state.context.count);

return (
// Send events to the actor
<button onClick={() => countActor.send({ type: 'inc' })}>
Count: {count}
</button>
);
}

You can read this global actor ("store") and send events to it from any component:

import { countActor } from './countActor';

export function Counter() {
const count = useSelector(countActor, (state) => state.context.count);

return (
<button onClick={() => countActor.send({ type: 'inc' })}>
Current count: {count}
</button>
);
}

Effects and lifecycles

Actors are not limited to being state stores. They can also be used to manage side effects, like HTTP requests, or to trigger side effects from within the actor. Because of this, you may not want actors to start immediately. You can use the .start() method to start the actor at an appropriate time, such as when the app mounts.

import { effectfulActor } from './effectfulActor';

export function App() {
useEffect(() => {
effectfulActor.start();
}, [effectfulActor]);

// ...
}

Likewise, you can also control when the actor stops by calling actor.stop().

Global state with React Context

If you prefer to use React Context to share global state, you can adapt the above pattern to use a React Context provider and consumer.

import { createContext } from 'react';

const someMachine = createMachine(/* ... */);
const someActor = createActor(someMachine);

// Don't forget to start the actor!
someActor.start();

// `someActor` is passed to `createContext` mostly for type safety
export const SomeActorContext = createContext(someActor);

export function App() {
return (
<SomeActorContext.Provider value={someActor}>
<Counter />
</SomeActorContext.Provider>
);
}
import { useContext } from 'react';
import { useSelector } from '@xstate/react';
import { SomeActorContext } from './SomeActorContext';

export function Counter() {
const someActor = useContext(SomeActorContext);
const count = useSelector(someActor, (state) => state.context.count);

return (
<button onClick={() => someActor.send({ type: 'inc' })}>
Current count: {count}
</button>
);
}

More than just state machines

State machines are very powerful and useful, but sometimes you don't need all that power. XState v5 enables you to make different kinds of actor logic to fit your use-cases, such as actor logic from promises, observables, transition functions, callbacks, and more.

import { fromTransition, createActor } from 'xstate';

// Actor logic
const counterLogic = fromTransition((state, event) => {
if (event.type === 'INC') {
return { count: state.count + 1 };
}
return state;
}, { count: 0 });

const counterActor = createActor(counterLogic);
counterActor.start();

// Same API
export function Counter() {
const count = useSelector(counterActor, (state) => state.context.count);

return (
<button onClick={() => counterActor.send({ type: 'inc' })}>
Current count: {count}
</button>
);
}