Zustand XState Middleware
raw JSON →Zustand middleware for integrating XState state machines into a global Zustand store. Currently at version 3.0.1, this package is designed to work seamlessly with XState v5 and Zustand v4, ensuring compatibility with their latest APIs and features. It provides a straightforward mechanism to create a Zustand store from an XState machine, exposing the machine's current `state`, the `send` event function, and the underlying `actor` for component consumption. This enables developers to leverage Zustand's reactive state management and selector capabilities to optimize re-renders while benefiting from XState's robust state orchestration. The middleware also allows passing actor options, such as `devTools`, directly to the XState actor, enhancing debugging and development workflows. While specific release cadences aren't explicitly stated, its alignment with major versions of its peer dependencies suggests active maintenance in response to their evolution.
Common errors
error TypeError: create is not a function ↓
import { create } from 'zustand' to import create from 'zustand'. error TypeError: machine.createMachine is not a function ↓
const machine = setup({ ... }).createMachine({ ... }); before passing machine to xstate(machine). error TypeError: Cannot read properties of undefined (reading 'value') when accessing state.value ↓
xstate (v5+) and zustand (v4+) are correctly installed and that the machine passed to the middleware is a valid XState v5 machine instance. Check console for earlier initialization errors. Warnings
breaking This middleware requires XState v5+ and Zustand v4+ as peer dependencies. Older versions of XState (e.g., v4) are not compatible due to significant API changes in XState v5, particularly around machine creation and actor management. ↓
gotcha When migrating from XState v4, remember that XState v5 introduces the `setup` API for machine definition. Directly passing old XState v4 machine definition objects will lead to runtime errors. ↓
gotcha The `create` function from `zustand` is a default export in v4+, not a named export. Incorrect import syntax will cause runtime errors. ↓
gotcha The `xstate` middleware expects an already created XState machine instance (the return value of `setup(...).createMachine(...)`), not just a plain machine configuration object. ↓
Install
npm install zustand-middleware-xstate yarn add zustand-middleware-xstate pnpm add zustand-middleware-xstate Imports
- create wrong
import { create } from 'zustand'correctimport create from 'zustand' - setup wrong
import setup from 'xstate'correctimport { setup } from 'xstate' - xstate wrong
import { xstate } from 'zustand-middleware-xstate'correctimport xstate from 'zustand-middleware-xstate'
Quickstart
import create from "zustand";
import { setup } from "xstate";
import xstate from "zustand-middleware-xstate";
type UserEvents = { type: 'LOGIN' } | { type: 'LOGOUT' };
// Create your XState machine definition using setup
const authMachine = setup({
types: {
events: {} as UserEvents,
context: {} as { userId: string | null }
},
}).createMachine({
id: "auth",
initial: "loggedOut",
context: { userId: null },
states: {
loggedOut: {
on: {
LOGIN: {
target: "loggedIn",
actions: ({ context }) => {
// In a real app, you'd set the actual user ID
context.userId = 'some-user-id-' + Math.random().toFixed(2);
}
}
}
},
loggedIn: {
on: {
LOGOUT: {
target: "loggedOut",
actions: ({ context }) => {
context.userId = null;
}
}
}
}
}
});
// Create a Zustand store hook using the xstate middleware
const useAuthStore = create(
xstate(authMachine, { devTools: process.env.NODE_ENV === 'development' })
);
// Example usage in a component-like structure
const AuthComponent = () => {
const { state, send, actor } = useAuthStore();
console.log('Current Auth State:', state.value);
console.log('User ID:', state.context.userId);
return (
<div>
<p>Authentication Status: {String(state.value)}</p>
{state.matches('loggedOut') && (
<button onClick={() => send({ type: 'LOGIN' })}>Login</button>
)}
{state.matches('loggedIn') && (
<button onClick={() => send({ type: 'LOGOUT' })}>Logout</button>
)}
<p>Actor status: {actor.getSnapshot().status}</p>
</div>
);
};
// Simulate rendering
AuthComponent(); // Initial render
useAuthStore.getState().send({ type: 'LOGIN' }); // Simulate user login
AuthComponent(); // Re-render after login
useAuthStore.getState().send({ type: 'LOGOUT' }); // Simulate user logout
AuthComponent(); // Re-render after logout