@xstate/react
The @xstate/react package contains utilities for using XState with React.
Quick startβ
- Install
xstateand@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
useMachinehook:
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 XStateStateobject.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 returningundefinedif 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 returningundefinedif 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 aselectorfunction and optionalcomparefunction 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 optionaloptionsobject.
Returns a tuple of [state, send, service]:
state- Represents the current state of the machine as an@xstate/fsmStateMachine.Stateobject.send- A function that sends events to the running service.service- The created@xstate/fsmservice.
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