# Usage with React

XState can be used with React to:

  • Coordinate local state
  • Manage global state performantly
  • Consume data from other hooks

At Stately (opens new window), 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 (opens new window).

# Local state

Using React hooks (opens new window) are the easiest way to use state machines in your components. You can use the official @xstate/react (opens new window) 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 (opens new window).

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 GlobalStateContext = createContext({});

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.

# Utilizing 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. Tools like Redux (opens new window) use selectors (opens new window) 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 { GlobalStateContext } from './globalState';
import { useSelector } from '@xstate/react';

const loggedInSelector = (state) => {
  return state.matches('loggedIn');
};

export const SomeComponent = (props) => {
  const globalServices = useContext(GlobalStateContext);
  const isLoggedIn = useSelector(globalServices.authService, loggedInSelector);

  return isLoggedIn ? 'Logged In' : 'Logged Out';
};

If you need to send an event in the component that consumes a service, you can use the service.send(...) method directly:

import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
import { useSelector } from '@xstate/react';

const loggedInSelector = (state) => {
  return state.matches('loggedIn');
};

export const SomeComponent = (props) => {
  const globalServices = useContext(GlobalStateContext);
  const isLoggedIn = useSelector(globalServices.authService, loggedInSelector);
  // Get `send()` method from a service
  const { send } = globalServices.authService;

   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 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.

# Other hooks

XState's useMachine and useInterpret hooks can be used alongside others. Two patterns are most common:

# Named actions/services/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 services, 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 (opens new window) and swr (opens new window). 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 service 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 service is started via this.service.start().
  • When the component will unmount, the service is stopped via this.service.stop().
  • Events are sent to the service via this.service.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
  };

  service = interpret(toggleMachine).onTransition((current) =>
    this.setState({ current })
  );

  componentDidMount() {
    this.service.start();
  }

  componentWillUnmount() {
    this.service.stop();
  }

  render() {
    const { current } = this.state;
    const { send } = this.service;

    return (
      <button onClick={() => send('TOGGLE')}>
        {current.matches('inactive') ? 'Off' : 'On'}
      </button>
    );
  }
}
Last Updated: 9/17/2021, 11:12:17 AM