A collection of helpers for XState (with React).
npm install xstate-helpers
createReactContextHelpers
creates a set of helpers that make it easy to share and access a machine through React context.
Creates a React.Context
which provides the machine's interpreter and returns a Provider
for the context and useInterpreter()
, useSend()
, useActor()
, and useSelector()
hooks which are initialized to the machine.
// ExampleProvider.tsx
import React from 'react';
import { useInterpret } from '@xstate/react';
import { createReactContextHelpers } from 'xstate-helpers/react/createReactContextHelpers';
import { useErrorHandler } from 'react-error-boundary';
import { useAuth } from 'auth';
import { exampleMachine } from './example.machine';
export const {
Provider: ExampleProvider,
ReactContext: ExampleContext,
useInterpreter: useExampleInterpreter,
useActor: useExampleActor,
useSelector: useExampleSelector,
useSend: useExampleSend,
} = createReactContextHelpers('Example', (props: { name: string }) => {
const auth = useAuth();
const handleError = useErrorHandler();
const interpreter = useInterpret(exampleMachine, {
context: { name: props.name },
actions: {
handleCriticalError: (_, e) => handleError(e.data),
},
});
React.useEffect(() => {
interpreter.send({ type: 'SET_USER', user: auth.user });
}, [interpreter, auth.user]);
return interpreter;
});
export default ExampleProvider;
// App.tsx
import React from 'react';
import ExampleProvider, {
useExampleInterpreter,
useExampleActor,
useExampleSelector,
} from './ExampleProvider';
const App: React.FC = () => {
return (
<ExampleProvider name="Example">
<Component />
</ExampleProvider>
);
};
const Component: React.FC = () => {
// the raw interpreter
const interpreter = useExampleInterpreter();
// just the send method, for components that don't need to read state
const send = useExampleSend();
// a pre-bound `useActor()` hook for when you need the whole state and the send function
const [state, send] = useExampleActor();
// a better pre-bound selector, that preserves proper types when React.useCallback() is used!
// Favor using this over `useExampleActor()` when possible, because selectors cause rerender
// only when the selected value changes, while `useExampleActor()` rerenders on every machine change.
const name = useExampleSelector(React.useCallback(state => state.context.name, []));
// ...
};
An easy way to add the XState Inspector directly to your React app
import React from 'react';
import { XStateInspectLoader } from 'xstate-helpers/react/XStateInspectLoader';
const App = () => {
return (
<XStateInspectLoader>
<YourComponents />
</XStateInspectLoader>
);
};
Then you can enable/disable the inspector by using the browser's console:
XStateInspector.enable()
XStateInspector.disable()
You can also override the inspector options from the console. For example, on some pages you might want the Inspector to run in a separate window:
XStateInspector.overrideOptions({ iframe: false })
And then you can return to the default iframe behavior:
XStateInspector.overrideOptions(undefined)
Prop | Type | Description |
---|---|---|
wrapperElement |
string | DivElement |
Optional. DOM Selector string or DIVElement to put the inspector into. If ommitted, inspector is placed as the first child of <body> |
styles |
React.CSSProperties |
Optional. CSS styles for the inner wrapping DIV of the inspector iframe. |
initialIsEnabled |
boolean |
Optional. Should the inspector initialize open the first time it's used. Afterwards, the console XStateInspector.enabled()/disabled() API takes precedence |
forceEnabled |
boolean |
Optional. Force the inspector into enabled/disabled state, regardless of console XStateInspector setting. |
options |
object |
Optional. Pass options into @xstate/inspect - { url: 'https://statecharts.io/inspect', iframe: false } |
Depricated in favor of useStateCan()
Check if a state transition is available from the current machine state.
import { createMachine } from 'xstate';
import { useMachine } from '@xstate/react';
import { useIsXStateTransitionAvailable } from 'xstate-helpers/react/useIsXStateTransitionAvailable';
const [state, send, service] = useMachine(
createMachine({
initial: 'one',
states: {
one: {
on: {
GO_TO_STATE_TWO: 'two',
PARAMETIZED_GO_TO_STATE_TWO: {
target: 'two',
cond: (_, e) => e.parameter,
},
},
},
two: {
on: {
GO_TO_STATE_ONE: 'one',
},
},
},
}),
);
// true
useIsXStateTransitionAvailable(service, 'GO_TO_STATE_TWO');
// false
useIsXStateTransitionAvailable(service, 'GO_TO_STATE_ONE');
// TypeError, no such event exists
useIsXStateTransitionAvailable(service, 'I_DONT_EXIST');
// true
useIsXStateTransitionAvailable(service, {
type: 'PARAMETIZED_GO_TO_STATE_TWO',
parameter: true,
});
// false
useIsXStateTransitionAvailable(service, {
type: 'PARAMETIZED_GO_TO_STATE_TWO',
parameter: false,
});
Parameter | Type | Description |
---|---|---|
service |
An interpreted machine or a spawned machine | Required. |
event |
EventObject or EventType | Required. |
A hook that makes using state.can()
easier!
To use state.can()
in React component, you must take extra steps to avoid additional re-renders:
const eventToCheck = { type: 'SOME_EVENT' };
const result = useSelector(
service,
React.useCallback(state => state.can(eventToCheck), [JSON.stringify(eventToCheck)]),
);
This hook makes it easier:
const [state, send, service] = useMachine(
createMachine({
initial: 'one',
states: {
one: {
on: {
GO_TO_STATE_TWO: 'two',
PARAMETIZED_GO_TO_STATE_TWO: {
target: 'two',
cond: (_, e) => e.parameter,
},
},
},
two: {
on: {
GO_TO_STATE_ONE: 'one',
},
},
},
}),
);
// true
useStateCan(service, 'GO_TO_STATE_TWO');
// false
useStateCan(service, 'GO_TO_STATE_ONE');
// TypeError, no such event exists
useStateCan(service, 'I_DONT_EXIST');
// true
useStateCan(service, {
type: 'PARAMETIZED_GO_TO_STATE_TWO',
parameter: true,
});
// false
useStateCan(service, {
type: 'PARAMETIZED_GO_TO_STATE_TWO',
parameter: false,
});
Parameter | Type | Description |
---|---|---|
service |
An interpreted machine or a spawned machine | Required. |
event |
EventObject or EventType | Required. |
Force an event to be handled as if it was of a particular type. Will throw a runtime exception if the given event does not match the expected event.
import { createMachine } from 'xstate';
import { invariantEvent } from 'xstate-helpers/events';
const machine = createMachine<Context, Event>(
{
on: {
SHOW_DATE: { actions: 'showDate' },
UPDATE_DATE: { actions: 'updateDate' },
},
},
{
actions: {
showDate: (ctx, e) => {
// TypeError and a runtime error
invariantEvent(e, 'NON_EXISTANT');
},
updateDate: assign({
date: (_, e) => {
// Runtime error, because this action is called with the `UPDATE_DATE` event, not the `SHOW_DATE` event
invariantEvent(e, 'SHOW_DATE');
},
}),
},
},
);
Parameter | Type | Description |
---|---|---|
event |
EventObject |
Required. |
eventType(s) |
EventObject | EventType | EventObject[] | EventType[] |
Required. |
Check if an event matches the expected event type.
import { createMachine } from 'xstate';
import { isEvent } from 'xstate-helpers/events';
const machine = createMachine<Context, Event>(
{
on: {
SHOW_DATE: { actions: 'showDate' },
UPDATE_DATE: { actions: 'updateDate' },
},
},
{
actions: {
showDate: (ctx, e) => {
if (!isEvent(e, 'SHOW_DATE') return;
alert(ctx.date.toISOString());
},
updateDate: assign({
date: (_, e) => {
if (!isEvent(e, 'UPDATE_DATE')) return;
return e.date; // has type "Date"
},
}),
},
}
);
Parameter | Type | Description |
---|---|---|
event |
EventObject |
Required. |
eventType(s) |
EventObject | EventType | EventObject[] | EventType[] |
Required. |
Force an event to be handled as if it was of a particular type. This function does not enforce the type at runtime. It's used just to appease TypeScript.
Warning! This just blindly asserts a type, without any runtime validation. It's recommended to use
isEvent()
orinvariantEvent()
instead.
import { createMachine } from 'xstate';
import { assertEvent } from 'xstate-helpers/events';
const machine = createMachine<Context, Event>(
{
on: {
SHOW_DATE: { actions: 'showDate' },
UPDATE_DATE: { actions: 'updateDate' },
},
},
{
actions: {
showDate: (ctx, e) => {
assertEvent(e, 'SHOW_DATE');
alert(ctx.date.toISOString());
},
updateDate: assign({
date: (_, e) => {
assertEvent(e, 'UPDATE_DATE');
return e.date; // has type "Date"
},
}),
},
},
);
Parameter | Type | Description |
---|---|---|
event |
EventObject |
Required. |
eventType(s) |
EventObject | EventType | EventObject[] | EventType[] |
Required. |