Minimal XState with Hooks
Replacing the reducer with declarative state management.
Over the last couple of years, we've started pushing our application state management to the browser. Managing state can be a large part of what makes our job as UI developers challenging. We've made great strides with patterns such as Redux and the useReducer hook in React.
I like Redux and
useReducer
Note: This is not intended to be an introduction to reducers or state machines, rather a light comparison between the two used in the context of React. If you are new to either concept, here are a few resources to dive into first
- React useReducer hook
- Xstate concepts
Using a reducer
Here is a simple toggle button example showcasing the reducer pattern with the
useReducer
import React, { useReducer } from 'react'; const reducer = (state, action) => { switch (action.type) { case 'TOGGLE': return state === 'active' ? 'inactive' : 'active'; default: return state; } }; const Toggler = () => { const [state, dispatch] = useReducer(reducer, 'inactive'); return ( <button onClick={() => dispatch({ type: 'TOGGLE' })}> {state === 'inactive' ? 'Activate' : 'Deactivate'} </button> ); }; export default Toggler;
A quick breakdown, the
useReducer
dispatch
action
In this example, there is only one action type that our reducer will respond (update) to,
TOGGLE
dispatch
{ type: 'TOGGLE' }
'active'
'inactive'
It works, but the switch statement and internal conditionals make it very procedural. To me, this seems manual and prone to leaving edge-cases unhandled.
A simple FSM alternative
Let's migrate our reducer pattern over to using a finite state machine. The
@xstate/react
@xstate/fsm
Instead of including all of XState plus the React package helpers, we can use the much smaller
@xstate/fsm
useMachine
Using a React hook makes it easier to use state machines with function components. As suggested in the Usage with React section of the XState docs, there are other hook-based solutions you can use or you can implement your own simple hook to interpret and use XState machines:
import { useState, useMemo, useEffect } from 'react'; import { interpret } from '@xstate/fsm'; export function useMachine(machine) { // Keep track of the current machine state const [current, setCurrent] = useState(machine.initialState); // Interpret the machine and start the service (only once!) const service = useMemo(() => interpret(machine).start(), [machine]); useEffect(() => { // Subscribe to state changes service.subscribe((state) => { // Update the current machine state when // a transition occurs if (state.changed) { setCurrent(state); } }); // Stop the service when the component unmounts return () => service.stop(); }, [service]); return [current, service.send]; }
Let's break it down a bit:
- Our machine's current state value is kept with thehook.
useState - The provided machine configuration is interpreted and then started.
- Our subscription is notified of all state changes, updating our hook state when a transition occurs.
- Our hook returns an array containing the current state and our service's [send] method so we can send transitions to our machine.
Using our minimal machine hook
Now that we have our custom
useMachine
import React from 'react'; import { createMachine } from '@xstate/fsm'; import { useMachine } from '../hooks/useMachine'; const toggleMachine = createMachine({ id: 'toggle', initial: 'inactive', states: { inactive: { on: { TOGGLE: 'active' } }, active: { on: { TOGGLE: 'inactive' } }, }, }); const Toggler = () => { const [state, send] = useMachine(toggleMachine); return ( <button onClick={() => send('TOGGLE')}> {state.value === 'inactive' ? 'Activate' : 'Deactivate'} </button> ); }; export default Toggler;
With a single, simple hook and the tiny
@xstate/fsm
Visualizing our state machine
XState also has a visualizer that allows you to load and preview your state machine configuration. The visualizer can create a sharable link of our toggle machine.
Instead of using a framework-specific API such as
useReducer
Upgrade ready
This is only a small step towards replacing reducer logic with a more declarative machine-based pattern. If you want to use all the statechart features such as nested states, parallel states, history states, activities, invoked services, delayed transitions, and transient transitions, we'll need to level up to the full XState library.
First, let's update our packages:
npm rm @xstate/fsm npm install -S xstate @xstate/react
Now, we can update our Toggler component code:
- Replacewith
@xstate/fsmxstate - Replace thefunction with the
createMachinefactory function fromMachinexstate - Use thehook instead of our custom
@xstate/reacthookuseMachine
import React from 'react'; import { Machine } from 'xstate'; import { useMachine } from '@xstate/react'; const toggleMachine = Machine({ id: 'toggle', initial: 'inactive', states: { inactive: { on: { TOGGLE: 'active' } }, active: { on: { TOGGLE: 'inactive' } }, }, }); const Toggler = () => { const [state, send] = useMachine(toggleMachine); return ( <button onClick={() => send('TOGGLE')}> {state.value === 'inactive' ? 'Activate' : 'Deactivate'} </button> ); }; export default Toggler;
That's it! Now we have all the more advanced capabilities of XState without having to make a single change to our state logic.
Here is an example CodeSandbox comparing the
useReducer
useMachine
@xstate/fsm
Subscribe to the newsletter
Be the first to know when I post something new! Thoughts about code, design, startups and other interesting things.