@xstate/react
The @xstate/react package contains utilities for using XState with React.
Quick startβ
- Install
xstate
and@xstate/react
:
npm i xstate @xstate/react
Via CDNβ
<script src="https://unpkg.com/@xstate/react/dist/xstate-react.umd.min.js"></script>
By using the global variable XStateReact
or
<script src="https://unpkg.com/@xstate/react/dist/xstate-react-fsm.umd.min.js"></script>
By using the global variable XStateReactFSM
- Import the
useMachine
hook:
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' },
},
},
});
export const Toggler = () => {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{state.value === 'inactive'
? 'Click to activate'
: 'Active! Click to deactivate'}
</button>
);
};
Examplesβ
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 or a function that lazily returns a machine:// existing machine
const [state, send] = useMachine(machine);
// lazily-created machine
const [state, send] = useMachine(() =>
createMachine({
/* ... */
}),
); -
options
(optional) - Interpreter options and/or any of the following machine config options:guards
,actions
,services
,delays
,immediate
,context
,state
. If the machine already contains any of these options, they will be merged, with these options taking precedence.
Returns a tuple of [state, send, service]
:
state
- Represents the current state of the machine as an XStateState
object.send
- A function that sends events to the running service.service
- The created service.
useActor(actor, getSnapshot?)
β
A React hook that subscribes to emitted changes from an existing actor.
Argumentsβ
actor
- an actor-like object that contains.send(...)
and.subscribe(...)
methods.getSnapshot
- a function that should return the latest emitted value from theactor
.- Defaults to attempting to get the
actor.state
, or returningundefined
if that does not exist.
- Defaults to attempting to get the
const [state, send] = useActor(someSpawnedActor);
// with custom actors
const [state, send] = useActor(customActor, (actor) => {
// implementation-specific pseudocode example:
return actor.getLastEmittedValue();
});
useInterpret(machine, options?, observer?)
β
A React hook that returns the service
created from the machine
with the options
, if specified. It starts the service and runs it for the lifetime of the component. This is similar to useMachine
; however, useInterpret
allows for a custom observer
to subscribe to the service
.
The useInterpret
is useful when you want fine-grained control, e.g. to add logging, or minimize re-renders. In contrast to useMachine
that would flush each update from the machine to the React component, useInterpret
instead returns a static reference (to just the interpreted machine) which will not rerender when its state changes.
To use a piece of state from the service inside a render, use the useSelector(...)
hook to subscribe to it.
Since 1.3.0
Argumentsβ
machine
- An XState machine or a function that lazily returns a machine.options
(optional) - Interpreter options and/or any of the following machine config options:guards
,actions
,services
,delays
,immediate
,context
,state
. If the machine already contains any of these options, they will be merged, with these options taking precedence.observer
(optional) - an observer or listener that listens to state updates:- an observer (e.g.,
{ next: (state) => {/* ... */} }
) - or a listener (e.g.,
(state) => {/* ... */}
)
- an observer (e.g.,
import { useInterpret } from '@xstate/react';
import { someMachine } from '../path/to/someMachine';
const App = () => {
const service = useInterpret(someMachine);
// ...
};
With options + listener:
// ...
const App = () => {
const service = useInterpret(
someMachine,
{
actions: {
/* ... */
},
},
(state) => {
// subscribes to state changes
console.log(state);
},
);
// ...
};
useSelector(actor, selector, compare?, getSnapshot?)
β
A React hook that returns the selected value from the snapshot of an actor
, such as a service. This hook will only cause a rerender if the selected value changes, as determined by the optional compare
function.
Since 1.3.0
Argumentsβ
actor
- a service or an actor-like object that contains.send(...)
and.subscribe(...)
methods.selector
- a function that takes in an actorβs "current state" (snapshot) as an argument and returns the desired selected value.compare
(optional) - a function that determines if the current selected value is the same as the previous selected value.getSnapshot
(optional) - a function that should return the latest emitted value from theactor
.- Defaults to attempting to get the
actor.state
, or returningundefined
if that does not exist. Will automatically pull the state from services.
- Defaults to attempting to get the
import { useSelector } from '@xstate/react';
// tip: optimize selectors by defining them externally when possible
const selectCount = (state) => state.context.count;
const App = ({ service }) => {
const count = useSelector(service, selectCount);
// ...
};
With compare
function:
// ...
const selectUser = (state) => state.context.user;
const compareUser = (prevUser, nextUser) => prevUser.id === nextUser.id;
const App = ({ service }) => {
const user = useSelector(service, selectUser, compareUser);
// ...
};
createActorContext(machine)
β
Since 3.1.0
Returns a React Context object that interprets the machine
and makes the interpreted actor available through React Context. There are helper methods for accessing state and the actor ref.
Argumentsβ
machine
- An XState machine or a function that lazily returns a machine.
Returns a React Context object that contains the following properties:
Provider
- a React Context Provider component with the following props:machine
- An XState machine that must be of the same type as the machine passed tocreateActorContext(...)
useActor()
- a React hook that returns a tuple of[state, send]
from the React ContextuseSelector(selector, compare?)
- a React hook that takes in aselector
function and optionalcompare
function and returns the selected value from the actor snapshotuseActorRef()
- a React hook that returns the actor ref of the interpretedmachine
Creating a React Context for the actor and providing it in app scope:
import { createActorContext } from '@xstate/react';
import { someMachine } from '../path/to/someMachine';
const SomeMachineContext = createActorContext(someMachine);
function App() {
return (
<SomeMachineContext.Provider>
<SomeComponent />
</SomeMachineContext.Provider>
);
}
Consuming the actor in a component:
import { SomeMachineContext } from '../path/to/SomeMachineContext';
function SomeComponent() {
// Read full snapshot and get `send` function from `useActor()`
const [state, send] = SomeMachineContext.useActor();
// Or derive a specific value from the snapshot with `useSelector()`
const count = SomeMachineContext.useSelector((state) => state.context.count);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => send('INCREMENT')}>Increment</button>
</div>
);
}
Reading the actor ref:
import { SomeMachineContext } from '../path/to/SomeMachineContext';
function SomeComponent() {
const actorRef = SomeMachineContext.useActorRef();
return (
<div>
<button onClick={() => actorRef.send('INCREMENT')}>Increment</button>
</div>
);
}
Providing a similar machine:
import { SomeMachineContext } from '../path/to/SomeMachineContext';
import { someMachine } from '../path/to/someMachine';
function SomeComponent() {
return (
<SomeMachineContext.Provider
machine={() =>
someMachine.withConfig({
/* ... */
})
}
>
<SomeOtherComponent />
</SomeMachineContext.Provider>
);
}
Shallow comparisonβ
The default comparison is a strict reference comparison (===
). If your selector returns non-primitive values, such as objects or arrays, you should keep this in mind and either return the same reference, or provide a shallow or deep comparator.
The shallowEqual(...)
comparator function is available for shallow comparison:
import { useSelector, shallowEqual } from '@xstate/react';
// ...
const selectUser = (state) => state.context.user;
const App = ({ service }) => {
// shallowEqual comparator is needed to compare the object, whose
// reference might change despite the shallow object values being equal
const user = useSelector(service, selectUser, shallowEqual);
// ...
};
With useInterpret(...)
:
import { useInterpret, useSelector } from '@xstate/react';
import { someMachine } from '../path/to/someMachine';
const selectCount = (state) => state.context.count;
const App = ({ service }) => {
const service = useInterpret(someMachine);
const count = useSelector(service, selectCount);
// ...
};
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/fsm
Argumentsβ
machine
- An XState finite state machine (FSM).options
- An optionaloptions
object.
Returns a tuple of [state, send, service]
:
state
- 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/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 [state, send] = useMachine(fetchMachine, {
actions: {
load: () => {
onFetch().then((res) => {
send({ type: 'RESOLVE', data: res });
});
},
},
});
switch (state.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">{state.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 = createMachine({
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 [state, send] = useMachine(fetchMachine, {
actions: {
notifySuccess: (ctx) => onResolve(ctx.data),
},
services: {
fetchData: (_, e) =>
fetch(`some/api/${e.query}`).then((res) => res.json()),
},
});
switch (state.value) {
case 'idle':
return (
<button onClick={() => send({ type: 'FETCH', query: 'something' })}>
Search for something
</button>
);
case 'loading':
return <div>Searching...</div>;
case 'success':
return <div>Success! Data: {state.context.data}</div>;
case 'failure':
return (
<>
<p>{state.context.error.message}</p>
<button onClick={() => send('RETRY')}>Retry</button>
</>
);
default:
return null;
}
};
Matching statesβ
When using hierarchical and parallel machines, the state values will be objects, not strings. In this case, it is best to use state.matches(...)
.
We can do this with if/else if/else
blocks:
// ...
if (state.matches('idle')) {
return /* ... */;
} else if (state.matches({ loading: 'user' })) {
return /* ... */;
} else if (state.matches({ loading: 'friends' })) {
return /* ... */;
} else {
return null;
}
We can also continue to use switch
, but we must make an adjustment to our approach. By setting the expression of the switch
to true
, we can use state.matches(...)
as a predicate in each case
:
switch (true) {
case state.matches('idle'):
return /* ... */;
case state.matches({ loading: 'user' }):
return /* ... */;
case state.matches({ loading: 'friends' }):
return /* ... */;
default:
return null;
}
A ternary statement can also be considered, especially within rendered JSX:
const Loader = () => {
const [state, send] = useMachine(/* ... */);
return (
<div>
{state.matches('idle') ? (
<Loader.Idle />
) : state.matches({ loading: 'user' }) ? (
<Loader.LoadingUser />
) : state.matches({ loading: 'friends' }) ? (
<Loader.LoadingFriends />
) : null}
</div>
);
};
Persisted and rehydrated Stateβ
You can persist and rehydrate state with useMachine(...)
via options.state
:
// ...
// Get the persisted state config object from somewhere, e.g. localStorage
const persistedState = JSON.parse(localStorage.getItem('some-persisted-state-key')) || someMachine.initialState;
const App = () => {
const [state, send] = useMachine(someMachine, {
state: persistedState // provide persisted state config object here
});
// state will initially be that persisted state, not the machineβs initialState
return (/* ... */)
}
Servicesβ
The service
created in useMachine(machine)
can be referenced as the third returned value:
// vvvvvvv
const [state, send, service] = useMachine(someMachine);
You can subscribe to that serviceβs state changes with the useEffect
hook:
// ...
useEffect(() => {
const subscription = service.subscribe((state) => {
// simple state logging
console.log(state);
});
return subscription.unsubscribe;
}, [service]); // note: service should never change