@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 interprets the given machine
and starts an actor that runs for the lifetime of the component.
Argumentsβ
machine
- An XState machineoptions?
- Interpreter 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 interprets the given actorLogic
and starts an actor that runs for the lifetime of the component.
Argumentsβ
actorLogic
- The actor logic to interpret; e.g.createMachine(...)
,fromPromise(...)
, etc.options?
(optional) - Interpreter 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 interpreted machine) 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) - Interpreter 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 interprets 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 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() {
const count = SomeMachineContext.useSelector((state) => state.context.count);
const someactorRef = SomeMachineContext.useActor();
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