Clockwork mechanism with React logo and a cat

Orchestrating React Apps with XState: A Symphony of State Machines

The Orange Cat
The Orange Cat

Introduction

In the ever-evolving landscape of React state management, @xstate/react emerges as a powerful tool for developers seeking to bring order to complex application logic. By leveraging the principles of finite state machines and statecharts, @xstate/react offers a structured, predictable approach to handling state transitions and side effects in React applications.

Features

@xstate/react brings several key features to the table that make it a compelling choice for state management:

  • Declarative State Machines: Define your application’s behavior using clear, declarative state machines.
  • React Hooks Integration: Seamlessly integrate state machines into your React components with custom hooks.
  • Visualizable Logic: Use the XState Visualizer to see and understand your application’s state flow.
  • Type-Safe: Fully typed with TypeScript for enhanced developer experience and code reliability.
  • Actor Model Support: Implement complex systems using hierarchical and parallel state machines.

Installation

To get started with @xstate/react, you’ll need to install both xstate and @xstate/react:

npm install xstate @xstate/react

Or if you prefer yarn:

yarn add xstate @xstate/react

Basic Usage

Let’s dive into how you can use @xstate/react in your React applications with some practical examples.

Creating a Simple Toggle Machine

First, let’s create a basic toggle machine:

import { createMachine } from 'xstate';

const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: {
      on: { TOGGLE: 'active' }
    },
    active: {
      on: { TOGGLE: 'inactive' }
    }
  }
});

Using the Machine in a React Component

Now, let’s use this machine in a React component using the useMachine hook:

import React from 'react';
import { useMachine } from '@xstate/react';

function ToggleButton() {
  const [state, send] = useMachine(toggleMachine);

  return (
    <button onClick={() => send('TOGGLE')}>
      {state.value === 'inactive' ? 'Turn On' : 'Turn Off'}
    </button>
  );
}

In this example, the useMachine hook interprets the toggleMachine and returns the current state and a send function to dispatch events to the machine.

Adding Context and Actions

Let’s enhance our toggle machine with some context and actions:

import { createMachine, assign } from 'xstate';

const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  context: {
    timesToggled: 0
  },
  states: {
    inactive: {
      on: {
        TOGGLE: {
          target: 'active',
          actions: 'incrementToggle'
        }
      }
    },
    active: {
      on: {
        TOGGLE: {
          target: 'inactive',
          actions: 'incrementToggle'
        }
      }
    }
  }
}, {
  actions: {
    incrementToggle: assign({
      timesToggled: (context) => context.timesToggled + 1
    })
  }
});

Now our component can access and display the number of times the button has been toggled:

function ToggleButton() {
  const [state, send] = useMachine(toggleMachine);

  return (
    <div>
      <button onClick={() => send('TOGGLE')}>
        {state.value === 'inactive' ? 'Turn On' : 'Turn Off'}
      </button>
      <p>Toggled {state.context.timesToggled} times</p>
    </div>
  );
}

Advanced Usage

@xstate/react isn’t limited to simple state machines. It excels at managing complex application logic. Let’s explore some advanced use cases.

Implementing a Fetch Machine

Here’s an example of a machine that handles data fetching with loading, success, and error states:

import { createMachine, assign } from 'xstate';

const fetchMachine = createMachine({
  id: 'fetch',
  initial: 'idle',
  context: {
    data: null,
    error: null
  },
  states: {
    idle: {
      on: { FETCH: 'loading' }
    },
    loading: {
      invoke: {
        src: 'fetchData',
        onDone: {
          target: 'success',
          actions: assign({ data: (_, event) => event.data })
        },
        onError: {
          target: 'failure',
          actions: assign({ error: (_, event) => event.data })
        }
      }
    },
    success: {
      on: { FETCH: 'loading' }
    },
    failure: {
      on: { FETCH: 'loading' }
    }
  }
});

Using the Fetch Machine in a Component

Now let’s use this fetch machine in a component:

import React from 'react';
import { useMachine } from '@xstate/react';

function DataFetcher() {
  const [state, send] = useMachine(fetchMachine, {
    services: {
      fetchData: async () => {
        const response = await fetch('https://api.example.com/data');
        if (!response.ok) throw new Error('Failed to fetch');
        return response.json();
      }
    }
  });

  return (
    <div>
      {state.matches('idle') && <button onClick={() => send('FETCH')}>Fetch Data</button>}
      {state.matches('loading') && <p>Loading...</p>}
      {state.matches('success') && <pre>{JSON.stringify(state.context.data, null, 2)}</pre>}
      {state.matches('failure') && <p>Error: {state.context.error.message}</p>}
    </div>
  );
}

This component uses the fetch machine to manage the entire lifecycle of a data fetching operation, including handling loading states and errors.

Parallel States

XState also supports parallel states, which are useful when you have multiple independent processes running simultaneously:

import { createMachine } from 'xstate';

const parallelMachine = createMachine({
  id: 'parallel',
  type: 'parallel',
  states: {
    power: {
      initial: 'off',
      states: {
        on: { on: { TOGGLE_POWER: 'off' } },
        off: { on: { TOGGLE_POWER: 'on' } }
      }
    },
    volume: {
      initial: 'low',
      states: {
        low: { on: { INCREASE: 'medium' } },
        medium: { on: { INCREASE: 'high', DECREASE: 'low' } },
        high: { on: { DECREASE: 'medium' } }
      }
    }
  }
});

This machine represents a device with independent power and volume controls. You can use it in a component like this:

function Device() {
  const [state, send] = useMachine(parallelMachine);

  return (
    <div>
      <button onClick={() => send('TOGGLE_POWER')}>
        Power: {state.value.power}
      </button>
      <button onClick={() => send('INCREASE')}>Increase Volume</button>
      <button onClick={() => send('DECREASE')}>Decrease Volume</button>
      <p>Current Volume: {state.value.volume}</p>
    </div>
  );
}

Conclusion

@xstate/react brings the power of state machines to React applications, offering a robust and predictable way to manage complex application logic. By defining clear states, transitions, and actions, developers can create more maintainable and less error-prone code.

Whether you’re building a simple toggle button or a complex data-fetching component, @xstate/react provides the tools to model your application’s behavior effectively. As you become more familiar with state machines and the actor model, you’ll find that @xstate/react opens up new possibilities for structuring and reasoning about your React applications.

Remember, the key to mastering @xstate/react is practice. Start with simple machines and gradually incorporate more advanced features as you become comfortable with the concepts. Happy coding!

Comments