@xstate/react
[[toc]]
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:
const [state, send] = useMachine(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
.
Returns a tuple of [state, send, service]
:
state
- 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)
::: warning Deprecated
In the next major version, useService(service)
will be replaced with useActor(service)
. Prefer using the useActor(service)
hook for services instead, since services are also actors.
Also, keep in mind that only a single argument (the event object) can be sent to send(eventObject)
from useActor(...)
. When migrating to useActor(...)
, refactor send(...)
calls to use only a single event object:
const [state, send] = useActor(service);
-send('CLICK', { x: 0, y: 3 });
+send({ type: 'CLICK', x: 0, y: 3 });
:::
A React hook that subscribes to state changes from an existing service.
Arguments
Returns a tuple of [state, send]
:
state
- Represents the current state of the service as an XState State
object.send
- A function that sends events to the running 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 the actor
.
- Defaults to attempting to get the
actor.state
, or returning undefined
if that does not exist.
const [state, send] = useActor(someSpawnedActor);
const [state, send] = useActor(customActor, (actor) => {
return actor.getLastEmittedValue();
});
useInterpret(machine, options?, observer?)
A React hook that returns the service
created from the machine
with the options
, if specified. It also sets up a subscription to the service
with the observer
, if provided.
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
.observer
(optional) - an observer or listener that listens to state updates:
- an observer (e.g.,
{ next: (state) => {/* ... */} }
) - or a listener (e.g.,
(state) => {/* ... */}
)
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) => {
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 the actor
.
- Defaults to attempting to get the
actor.state
, or returning undefined
if that does not exist. Will automatically pull the state from services.
import { useSelector } from '@xstate/react';
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);
};
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);
};
asEffect(action)
Ensures that the action
is executed as an effect in useEffect
, rather than being immediately executed.
Arguments
action
- An action function (e.g., (context, event) => { alert(context.message) })
)
Returns a special action function that wraps the original so that useMachine
knows to execute it in useEffect
.
Example
const machine = createMachine({
initial: 'focused',
states: {
focused: {
entry: 'focus'
}
}
});
const Input = () => {
const inputRef = useRef(null);
const [state, send] = useMachine(machine, {
actions: {
focus: asEffect((context, event) => {
inputRef.current && inputRef.current.focus();
})
}
});
return <input ref={inputRef} />;
};
asLayoutEffect(action)
Ensures that the action
is executed as an effect in useLayoutEffect
, rather than being immediately executed.
Arguments
action
- An action function (e.g., (context, event) => { alert(context.message) })
)
Returns a special action function that wraps the original so that useMachine
knows to execute it in useLayoutEffect
.
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
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('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
:
const persistedState = JSON.parse(localStorage.getItem('some-persisted-state-key')) || someMachine.initialState;
const App = () => {
const [state, send] = useMachine(someMachine, {
state: persistedState
});
return ()
}
Services
The service
created in useMachine(machine)
can be referenced as the third returned value:
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) => {
console.log(state);
});
return subscription.unsubscribe;
}, [service]);
Migration from 0.x
-
For spawned actors created using invoke
or spawn(...)
, use the useActor()
hook instead of useService()
:
-import { useService } from '@xstate/react';
+import { useActor } from '@xstate/react';
-const [state, send] = useService(someActor);
+const [state, send] = useActor(someActor);
Resources
State Machines in React