@xstate/react
The @xstate/react package contains hooks for using XState with React.
Quick startβ
- Install
xstate
and@xstate/react
:
- yarn
- npm
yarn add xstate@beta @xstate/react@beta
npm install xstate@beta @xstate/react@beta
- 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({ type: 'TOGGLE' })}>
{state.value === 'inactive'
? 'Click to activate'
: 'Active! Click to deactivate'}
</button>
);
};
Examplesβ
APIβ
useMachine(machine, options?)
β
A React hook that creates an actor from the given machine
and starts an actor that runs for the lifetime of the component.
Argumentsβ
machine
- An XState machineoptions?
- Actor options.
Returns a tuple of [state, send, actor]
:
state
- Represents the current state of the machine as an XStateState
object.send
- A function that sends events to the running actor.actor
- The started actor.
// Existing machine
const [state, send] = useMachine(machine);
// Machine with provided implementations
// Will keep provided implementations up-to-date
const [state, send] = useMachine(
machine.provide({
actions: {
doSomething: ({ context }) => {
// ...
},
},
}),
);
useActor(actorLogic)
β
A React hook that creates an actor from the the given actorLogic
and starts an actor that runs for the lifetime of the component.
Argumentsβ
actorLogic
- The actor logic to create an actor from; e.g.createMachine(...)
,fromPromise(...)
, etc.options?
(optional) - Actor options
const promiseLogic = fromPromise(async () => {
const data = await getData(...);
return data;
});
const [state, send] = useActor(promiseLogic);
useActorRef(machine, options?, observer?)
β
A React hook that returns the actorRef
created from the machine
with the options
, if specified. It starts the actor ref and runs it for the lifetime of the component. This is similar to useActor
; however, useActorRef
allows for a custom observer
to subscribe to the actorRef
.
The useActorRef
is useful when you want fine-grained control, e.g. to add logging, or minimize re-renders. In contrast to useActor
that would flush each update from the machine to the React component, useActorRef
instead returns a static reference (to just the machine actor) which will not rerender when its state changes.
To use a piece of state from the actorRef inside a render, use the useSelector(...)
hook to subscribe to it.
Argumentsβ
actorLogic
options?
(optional) - Actor optionsobserver?
(optional) - an observer or listener that listens to state updates:- an observer (e.g.,
{ next: (snapshot) => {/* ... */} }
) - or a listener (e.g.,
(snapshot) => {/* ... */}
)
- an observer (e.g.,
import { useActorRef } from '@xstate/react';
import { someMachine } from '../path/to/someMachine';
const App = () => {
const actorRef = useActorRef(someMachine);
// ...
};
Providing machine implementations:
// ...
const App = () => {
const actorRef = useActorRef(
someMachine.provide({
actions: {
// ...
},
}),
);
// ...
};
useSelector(actorRef, selector, compare?, getSnapshot?)
β
A React hook that returns the selected value from the snapshot of an actorRef
, such as a actor ref. This hook will only cause a rerender if the selected value changes, as determined by the optional compare
function.
Argumentsβ
actorRef
- an actor refselector
- a function that takes in an actorβs 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 actor refs.
- Defaults to attempting to get the
import { useSelector } from '@xstate/react';
// tip: optimize selectors by defining them externally when possible
const selectCount = (snapshot) => snapshot.context.count;
const App = ({ actorRef }) => {
const count = useSelector(actorRef, selectCount);
// ...
};
With compare
function:
// ...
const selectUser = (snapshot) => snapshot.context.user;
const compareUser = (prevUser, nextUser) => prevUser.id === nextUser.id;
const App = ({ actorRef }) => {
const user = useSelector(actorRef, selectUser, compareUser);
// ...
};
createActorContext(machine)
β
Returns a React Context object that creates an actor from the machine
and makes the actor available through React Context. There are helper methods for accessing state and the actor ref.
Argumentsβ
machine
- An XState 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(...)
useSelector(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 themachine
actor
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() {
const count = SomeMachineContext.useSelector((state) => state.context.count);
const someActorRef = SomeMachineContext.useActorRef();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => someActorRef.send({ type: 'inc' })}>
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.provide({
actions: {
someAction: differentImplementation,
},
// ... More implementations
})}
>
<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 = ({ actorRef }) => {
// shallowEqual comparator is needed to compare the object, whose
// reference might change despite the shallow object values being equal
const user = useSelector(actorRef, selectUser, shallowEqual);
// ...
};
With useActorRef(...)
:
import { useActorRef, useSelector } from '@xstate/react';
import { someMachine } from '../path/to/someMachine';
const selectCount = (state) => state.context.count;
const App = () => {
const actorRef = useActorRef(someMachine);
const count = useSelector(actorRef, selectCount);
// ...
};
Configuring machinesβ
Existing machines can be customized by providing different implementations in machine.provide(implementations)
.
Example: the 'fetchData'
actor ref 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.output,
}),
},
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.provide({
actions: {
notifySuccess: ({ context }) => onResolve(context.data),
},
actors: {
fetchData: fromPromise(() =>
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({ type: '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
in the 2nd argument:
// ...
// Get the persisted state config object from somewhere, e.g. localStorage
const persistedState = JSON.parse(localStorage.getItem('some-persisted-state-key'));
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 (/* ... */)
}
Actor refsβ
The actorRef
created in useMachine(machine)
can be referenced as the third returned value:
// vvvvvvvv
const [state, send, actorRef] = useMachine(someMachine);
You can subscribe to that actor ref's state changes with the useEffect
hook:
// ...
useEffect(() => {
const subscription = actorRef.subscribe((snapshot) => {
// simple logging
console.log(snapshot);
});
return subscription.unsubscribe;
}, [service]); // note: actor ref should never change
Resourcesβ
Coming soon