What is @xstate/react?
@xstate/react is a library that provides React hooks for using XState, a state machine and statechart library. It allows you to manage complex state logic in a more structured and predictable way by using finite state machines and statecharts.
What are @xstate/react's main functionalities?
Creating and Using State Machines
This feature allows you to create and use state machines in your React components. The code sample demonstrates a simple toggle button that switches between 'inactive' and 'active' states.
import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
function ToggleButton() {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{state.matches('inactive') ? 'Off' : 'On'}
</button>
);
}
Using Context for Extended State
This feature allows you to use context for extended state in your state machines. The code sample demonstrates a counter that increments and decrements a count value stored in the machine's context.
import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';
const counterMachine = createMachine({
id: 'counter',
initial: 'active',
context: { count: 0 },
states: {
active: {
on: {
INCREMENT: { actions: 'increment' },
DECREMENT: { actions: 'decrement' }
}
}
}
}, {
actions: {
increment: (context) => context.count++,
decrement: (context) => context.count--
}
});
function Counter() {
const [state, send] = useMachine(counterMachine);
return (
<div>
<p>{state.context.count}</p>
<button onClick={() => send('INCREMENT')}>Increment</button>
<button onClick={() => send('DECREMENT')}>Decrement</button>
</div>
);
}
Invoking Services
This feature allows you to invoke services (e.g., API calls) within your state machines. The code sample demonstrates a state machine that fetches data from an API and handles loading, success, and failure states.
import { useMachine } from '@xstate/react';
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: (context, event) => event.data }) },
onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }) }
}
},
success: { on: { FETCH: 'loading' } },
failure: { on: { FETCH: 'loading' } }
}
}, {
services: {
fetchData: () => fetch('/api/data').then(response => response.json())
}
});
function FetchData() {
const [state, send] = useMachine(fetchMachine);
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}</p>}
</div>
);
}
Other packages similar to @xstate/react
redux
Redux is a popular state management library for JavaScript applications. It provides a centralized store for state and uses actions and reducers to manage state changes. Unlike @xstate/react, which uses state machines and statecharts, Redux relies on a more traditional approach with a single global state and pure functions to handle state transitions.
mobx
MobX is a state management library that makes state observable and automatically updates the UI when the state changes. It uses reactive programming principles and provides a more flexible and less boilerplate-heavy approach compared to @xstate/react. While @xstate/react focuses on finite state machines, MobX emphasizes reactivity and simplicity.
recoil
Recoil is a state management library for React that provides a more fine-grained approach to state management. It allows you to create atoms (pieces of state) and selectors (derived state) that can be used in your components. Recoil offers a more granular and flexible way to manage state compared to the structured approach of state machines in @xstate/react.
@xstate/react
Quick Start
- Install
xstate
and @xstate/react
:
npm i xstate @xstate/react
- Import the
useMachine
hook:
import { useMachine } from '@xstate/react';
import { Machine } from 'xstate';
const toggleMachine = Machine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
on: { TOGGLE: 'active' }
},
active: {
on: { TOGGLE: 'inactive' }
}
}
});
export const Toggler = () => {
const [current, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{current.value === 'inactive'
? 'Click to activate'
: 'Active! Click to deactivate'}
</button>
);
};
API
useMachine(machine, options?)
A React hook that interprets the given machine
and starts a service that runs for the lifetime of the component.
Arguments
machine
- An XState machine.options
(optional) - Interpreter options OR one of the following Machine Config options: guards
, actions
, activities
, services
, delays
, immediate
, context
, or state
.
Returns a tuple of [current, send, service]
:
current
- Represents the current state of the machine as an XState State
object.send
- A function that sends events to the running service.service
- The created service.
useService(service)
A React hook that subscribes to state changes from an existing service.
Arguments
Returns a tuple of [current, send]
:
current
- Represents the current state of the service as an XState State
object.send
- A function that sends events to the running service.
useActor(actor)
A React hook that subscribes to messages (events) from actors, and can send messages (events) to actors.
Arguments
actor
- An actor-like object, which has .subscribe(listener)
and .send(event)
methods.
Returns a tuple of [current, send]
:
current
- Represents the current message sent from the actor.send
- A function that sends events to the actor.
useMachine(machine)
with @xstate/fsm
A React hook that interprets the given finite state machine
from [@xstate/fsm
] and starts a service that runs for the lifetime of the component.
This special useMachine
hook is imported from @xstate/react/lib/fsm
Arguments
Returns a tuple of [current, send, service]
:
current
- Represents the current state of the machine as an @xstate/fsm
StateMachine.State
object.send
- A function that sends events to the running service.service
- The created @xstate/fsm
service.
Example
import { useEffect } from 'react';
import { useMachine } from `@xstate/react/lib/fsm`;
import { createMachine } from '@xstate/fsm';
const context = {
data: undefined
};
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context,
states: {
idle: {
on: { FETCH: 'loading' }
},
loading: {
entry: ['load'],
on: {
RESOLVE: {
target: 'success',
actions: assign({
data: (context, event) => event.data
})
}
}
},
success: {}
}
});
const Fetcher = ({ onFetch = () => new Promise(res => res('some data')) }) => {
const [current, send] = useMachine(fetchMachine);
useEffect(() => {
current.actions.forEach(action => {
if (action.type === 'load') {
onFetch().then(res => {
send({ type: 'RESOLVE', data: res });
});
}
});
}, [current]);
switch (current.value) {
case 'idle':
return <button onClick={_ => send('FETCH')}>Fetch</button>;
case 'loading':
return <div>Loading...</div>;
case 'success':
return (
<div>
Success! Data: <div data-testid="data">{current.context.data}</div>
</div>
);
default:
return null;
}
};
Configuring Machines
Existing machines can be configured by passing the machine options as the 2nd argument of useMachine(machine, options)
.
Example: the 'fetchData'
service and 'notifySuccess'
action are both configurable:
const fetchMachine = Machine({
id: 'fetch',
initial: 'idle',
context: {
data: undefined,
error: undefined
},
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: {
entry: 'notifySuccess',
type: 'final'
},
failure: {
on: {
RETRY: 'loading'
}
}
}
});
const Fetcher = ({ onResolve }) => {
const [current, send] = useMachine(fetchMachine, {
actions: {
notifySuccess: ctx => onResolve(ctx.data)
},
services: {
fetchData: (_, e) => fetch(`some/api/${e.query}`).then(res => res.json())
}
});
switch (current.value) {
case 'idle':
return (
<button onClick={() => send('FETCH', { query: 'something' })}>
Search for something
</button>
);
case 'loading':
return <div>Searching...</div>;
case 'success':
return <div>Success! Data: {current.context.data}</div>;
case 'failure':
return (
<>
<p>{current.context.error.message}</p>
<button onClick={() => send('RETRY')}>Retry</button>
</>
);
default:
return null;
}
};
Matching States
Using a switch
statement might suffice for a simple, non-hierarchical state machine, but for hierarchical and parallel machines, the state values will be objects, not strings. In this case, it's better to use state.matches(...)
:
if (current.matches('idle')) {
return ;
} else if (current.matches({ loading: 'user' })) {
return ;
} else if (current.matches({ loading: 'friends' })) {
return ;
} else {
return null;
}
A ternary statement can also be considered, especially within rendered JSX:
const Loader = () => {
const [current, send] = useMachine();
return (
<div>
{current.matches('idle') ? (
<Loader.Idle />
) : current.matches({ loading: 'user' }) ? (
<Loader.LoadingUser />
) : current.matches({ loading: 'friends' }) ? (
<Loader.LoadingFriends />
) : null}
</div>
);
};
Persisted and Rehydrated State
You can persist and rehydrate state with useMachine(...)
via options.state
:
const persistedState = JSON.parse(localStorage.getItem('some-persisted-state-key'));
const App = () => {
const [current, send] = useMachine(someMachine, {
state: persistedState
});
return ()
}
Resources
State Machines in React