UNPKG

xstate-helpers

Version:

A collection of helpers for XState (with React).

385 lines (312 loc) • 11.6 kB
# 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/)