xstate-helpers
Version:
A collection of helpers for XState (with React).
385 lines (312 loc) • 11.6 kB
Markdown
# XState Helpers
A collection of helpers for XState (with React).
## Installation
```bash
npm install xstate-helpers
```
## API Reference
### createReactContextHelpers()
`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.
```typescript
// 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;
```
```typescript
// 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, []));
// ...
};
```
### XStateInspectLoader
An easy way to add the XState Inspector directly to your React app
```typescript
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)
```
#### `<XStateInspectLoader>` props:
| 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 }` |
### useIsXStateTransitionAvailable()
#### Deprecated
Depricated in favor of [`useStateCan()`](#useStateCan)
Check if a state transition is available from the current machine state.
```typescript
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**. |
### useStateCan()
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:
```typescript
const eventToCheck = { type: 'SOME_EVENT' };
const result = useSelector(
service,
React.useCallback(state => state.can(eventToCheck), [JSON.stringify(eventToCheck)]),
);
```
This hook makes it easier:
```typescript
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**. |
### invariantEvent()
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.
```typescript
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**. |
### isEvent()
Check if an event matches the expected event type.
```typescript
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**. |
### assertEvent()
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()` or `invariantEvent()` instead.
```typescript
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**. |
## License
[MIT](https://choosealicense.com/licenses/mit/)