How to manage global state with XState and React (2024)

Matt Poco*ck

Posted on • Updated on • Originally published at stately.ai

How to manage global state with XState and React (3) How to manage global state with XState and React (4) How to manage global state with XState and React (5) How to manage global state with XState and React (6) How to manage global state with XState and React (7)

#xstate #react #redux #webdev

This article has become part of the official XState docs!

Many React applications follow the Flux architecture popularised by Redux. This setup can be characterised by a few key ideas:

  1. It uses a single object at the top of your app which stores all application state, often called the store.
  2. It provides a single dispatch function which can be used to send messages up to the store. Redux calls these actions, but I'll be calling them events - as they're known in XState.
  3. How the store responds to these messages from the app are expressed in pure functions - most often in reducers.

This article won't go into depth on whether the Flux architecture is a good idea. David Khourshid's article Redux is half a pattern goes into great detail here. For the purposes of this article, we're going to assume that you like having a global store, and you want to replicate it in XState.

There are many reasons for wanting to do so. XState is second-to-none when it comes to managing complex asynchronous behaviour and modelling difficult problems. Managing this in Redux apps usually involves middleware: either redux-thunk, redux-loop or redux-saga. Choosing XState gives you a first-class way to manage complexity.

A globally available store

To mimic Redux's globally-available store, we're going to use React context. React context can be a tricky tool to work with - if you pass in values which change too often, in 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.

import React, { createContext } from 'react';import { useInterpret } from '@xstate/react';import { authMachine } from './authMachine';import { ActorRefFrom } from 'xstate';interface GlobalStateContextType { authService: ActorRefFrom<typeof authMachine>;}export const GlobalStateContext = createContext( // Typed this way to avoid TS errors, // looks odd I know {} as GlobalStateContextType,);export const GlobalStateProvider = (props) => { const authService = useInterpret(authMachine); return ( <GlobalStateContext.Provider value={{ authService }}> {props.children} </GlobalStateContext.Provider> );};

Using useInterpret returns a service, 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.

Utilising context

Further down the tree, you can subscribe to the service like this:

import React, { useContext } from 'react';import { GlobalStateContext } from './globalState';import { useActor } from '@xstate/react';export const SomeComponent = (props) => { const globalServices = useContext(GlobalStateContext); const [state] = useActor(globalServices.authService); return state.matches('loggedIn') ? 'Logged In' : 'Logged Out';};

The useActor hook listens for whenever the service 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 service. Redux offers tools for deriving state using selectors - functions which restrict which parts of the state can result in components re-rendering.

Luckily, XState provides that too.

import React, { useContext } from 'react';import { GlobalStateContext } from './globalState';import { useSelector } from '@xstate/react';const selector = (state) => { return state.matches('loggedIn');};export const SomeComponent = (props) => { const globalServices = useContext(GlobalStateContext); const isLoggedIn = useSelector(globalServices.authService, selector); return isLoggedIn ? 'Logged In' : 'Logged Out';};

Now, this component will only re-render when state.matches('loggedIn') returns a different value. This is my recommended approach over useActor for when you want to optimise performance.

Dispatching events

For dispatching events to the global store, you can call a service's send function directly.

import React, { useContext } from 'react';import { GlobalStateContext } from './globalState';export const SomeComponent = (props) => { const globalServices = useContext(GlobalStateContext); return ( <button onClick={() => globalServices.authService.send('LOG_OUT')}> Log Out </button> );};

Note that you don't need to call useActor for this, it's available right on the context.

Deviations from Flux

Keen-eyed readers may spot that this implementation is slightly different from Flux. For instance - instead of a single global store, one might have several running machines at once: authService, dataCacheService, and globalTimeoutService. Each of them have their own send attributes, too - so you're not calling a global dispatch.

These changes can be worked around. One could create a synthetic send inside the global store which called all the services' send function manually. But personally, I prefer knowing exactly which services my messages are being passed to, and it avoids having to keep events globally namespaced.

Summary

XState can work beautifully as a global store for a React application. It keeps application logic co-located, treats side effects as first-class citizens, and offers good performance with useSelector. You should choose this approach if you're keen on the Flux architecture but feel your app's logic is getting out of hand.

Top comments (19)

Subscribe

Emanuel Quimper

Emanuel Quimper

  • Location

    Quebec/Canada

  • Work

    Full-Stack Developer at AppAndFlow

  • Joined

Jul 16 '21

  • Copy link

Thank you for this. Really love it. Question for you, from what I see this is impossible to inject later some services like you will do with a simple useMachine when it's local machine right ? What I mean by that it's I want to use it as global like you do but would like the services to be inject in the component where those thing happen. But I don't think it's possible.

Matt Poco*ck

Matt Poco*ck

TypeScript Wizard, author of Total TypeScript.

  • Location

    Oxford, UK

  • Work

    Full-time Educator

  • Joined

Jul 16 '21

  • Copy link

Could you provide some code to explain what you mean? What are you looking to achieve?

Emanuel Quimper

Emanuel Quimper

  • Location

    Quebec/Canada

  • Work

    Full-Stack Developer at AppAndFlow

  • Joined

Jul 17 '21

  • Copy link

Something like you do with useMachine

const globalServices = useContext(GlobalStateContext);const [state, send] = useMachine(globalServices.authService.machine, { services: { myService }})

But doing it won't work cause this look like create a new instance

Matt Poco*ck

Matt Poco*ck

TypeScript Wizard, author of Total TypeScript.

  • Location

    Oxford, UK

  • Work

    Full-time Educator

  • Joined

Jul 17 '21

  • Copy link

What would you expect to happen in this instance? I am still confused by what you're trying to achieve.

You're correct, the above code won't work.

Emanuel Quimper

Emanuel Quimper

  • Location

    Quebec/Canada

  • Work

    Full-Stack Developer at AppAndFlow

  • Joined

Jul 21 '21

  • Copy link

What I try to do it's in my case the service are apollo mutation. I don't want to upfront all the mutation need in this global service. By doing this way I would be able to control the pattern from the component but getting it to work with the global machine.

Matt Poco*ck

Matt Poco*ck

TypeScript Wizard, author of Total TypeScript.

  • Location

    Oxford, UK

  • Work

    Full-time Educator

  • Joined

Jul 21 '21

  • Copy link

Why do you want the apollo mutation to be held in the global service? It feels more natural to me to have it in the component where it's used.

Emanuel Quimper

Emanuel Quimper

  • Location

    Quebec/Canada

  • Work

    Full-Stack Developer at AppAndFlow

  • Joined

Jul 22 '21

  • Copy link

I want my service who is global for all the onboarding part to handle the logic and make component quite simple.

Matt Poco*ck

Matt Poco*ck

TypeScript Wizard, author of Total TypeScript.

  • Location

    Oxford, UK

  • Work

    Full-time Educator

  • Joined

Jul 23 '21

  • Copy link

Right - I think that's a mistake. Instead, you should keep truly local state local. I would make a state machine inside the component to handle this.

Emanuel Quimper

Emanuel Quimper

  • Location

    Quebec/Canada

  • Work

    Full-Stack Developer at AppAndFlow

  • Joined

Jul 25 '21

  • Copy link

Cause I was trying to implement like you show in the xstate catalog with the multi steps form. Was working well until this issue. Thank you

Paul Berg

Paul Berg

Founder @ Sablier

  • Location

    Romania

  • Work

    Software Engineer at Sablier

  • Joined

Jul 18 '21

  • Copy link

The snippet that shows how to use useActor may be outdated:

TypeError: Cannot use 'in' operator to search for 'getSnapshot' in undefined

This is with react@17.0.2, xstate@4.23.0 and @xstate/react@npm:1.5.1.

How to manage global state with XState and React (29)

Comment deleted

Paul Berg

Paul Berg

Founder @ Sablier

  • Location

    Romania

  • Work

    Software Engineer at Sablier

  • Joined

Aug 9 '21

  • Copy link

The issue is that you're not consuming the context provider.

github.com/statelyai/xstate/issues...

Fernando Andrés García Hernández

Fernando Andrés García Hernández

  • Joined

May 27 '21

  • Copy link

That looks really nice! I have been using state machines with react for a few weeks now too and I'm in love with them.

I really liked your implementation of the Context and I didn't know about the useSelector, so thanks a lot for the help :)

Matt Poco*ck

Matt Poco*ck

TypeScript Wizard, author of Total TypeScript.

  • Location

    Oxford, UK

  • Work

    Full-time Educator

  • Joined

May 27 '21

  • Copy link

Thanks Fernando!

Petar Kolev

Petar Kolev

  • Joined

May 6 '23

  • Copy link

state inconst selector = (state) => {
return state.matches('loggedIn');
};
isn't typed. What type we should use on it? TS is complaining about it. Also I don't have type support in the matches method. How to apply one?

Richard Moore

Richard Moore

  • Joined

Feb 12 '22 • Edited on Feb 12 • Edited

  • Copy link

For anyone having TypeScript issues with this in XState 4.29+ (where there are new schema and tsTypes attributes on the machine, see xstate.js.org/docs/guides/typescri...), ActorRefFrom no longer seems to work but you can use InterpreterFrom in its place

type GlobalStateContextType = { authService: InterpreterFrom<typeof authMachine>;};

Matt Poco*ck

Matt Poco*ck

TypeScript Wizard, author of Total TypeScript.

  • Location

    Oxford, UK

  • Work

    Full-time Educator

  • Joined

Mar 1 '22

  • Copy link

Looks like it's fixed now:

github.com/statelyai/xstate/commit...

José C. Flores

José C. Flores

  • Joined

Jul 1 '21

  • Copy link

My main struggle with XState is global state management. I like creating the services in a parent machine and pulling them out from context but this example is definitely clean and easy to follow. Thanks for sharing.

creativemacmac

creativemacmac

frontend developer who always has her nose in some sort of innovation or other :)

  • Location

    Norway

  • Work

    Web designer at self-employed

  • Joined

May 21 '22

  • Copy link

Xstate both looks and sounds phenomenal! 🤩 Thanks a mil for this

View full discussion (19 comments)

For further actions, you may consider blocking this person and/or reporting abuse

How to manage global state with XState and React (2024)

References

Top Articles
Latest Posts
Article information

Author: Corie Satterfield

Last Updated:

Views: 5934

Rating: 4.1 / 5 (42 voted)

Reviews: 89% of readers found this page helpful

Author information

Name: Corie Satterfield

Birthday: 1992-08-19

Address: 850 Benjamin Bridge, Dickinsonchester, CO 68572-0542

Phone: +26813599986666

Job: Sales Manager

Hobby: Table tennis, Soapmaking, Flower arranging, amateur radio, Rock climbing, scrapbook, Horseback riding

Introduction: My name is Corie Satterfield, I am a fancy, perfect, spotless, quaint, fantastic, funny, lucky person who loves writing and wants to share my knowledge and understanding with you.