react-xstate-hoc
TypeScript icon, indicating that this package has built-in type declarations

1.9.0 • Public • Published

npm version CircleCI

React Xstate HOC

Integrates the Xstate lib with Reactjs. Please follow this link for more details about Xstate https://xstate.js.org/docs/

DEMO

Demo app is available here: https://stackblitz.com/edit/react-xstate-hoc-test

Example

Please find the example here: https://github.com/sangallimarco/react-xstate-hoc/tree/master/src/features

Example includes a better usage of Types for Actions and States.

Install

npm i react-xstate-hoc

OR

yarn add react-xstate-hoc

HOW TO

Define your State Machine

// file: configs/test-machine.ts
 
import { StateMachineAction } from 'react-xstate-hoc';
import { MachineConfig, assign } from 'xstate';
 
export interface TestComponentState {
    items: string[];
}
 
export interface TestMachineStateSchema {
    states: {
        START: {};
        PROCESSING: {};
        LIST: {};
        ERROR: {};
        SHOW_ITEM: {};
    }
}
 
export type TestMachineEvents = 
    | { type: 'SUBMIT', extra: string }
    | { type: 'CANCEL' }
    | { type: 'RESET' }
    | { type: 'SELECT' }
    | { type: 'EXIT' };
 
export const STATE_CHART: MachineConfig<TestComponentState, TestMachineStateSchema, TestMachineEvents> = {
    id: 'test',
    initial: 'START',
    states: {
        START: {
            on: {
                SUBMIT: {
                    target: 'PROCESSING',
                    cond: (ctx: TestComponentState) => {
                        return ctx.items.length === 0;
                    }
                }
            },
            onEntry: assign({
                items: []
            })
        },
        PROCESSING: {
            invoke: {
                src: 'FETCH_DATA',
                onDone: {
                    target: 'LIST',
                    actions: assign({
                        items: (ctx: TestComponentState, event: StateMachineAction<TestComponentState>) => {
                            return event.data.items;
                        }
                    })
                },
                onError: {
                    target: 'ERROR'
                    // error: (ctx, event) => event.data
                }
            }
        },
        LIST: {
            on: {
                RESET: 'START',
                SELECT: 'SHOW_ITEM'
            }
        },
        SHOW_ITEM: {
            on: {
                EXIT: 'LIST'
            }
        },
        ERROR: {
            on: {
                RESET: 'START'
            }
        }
    }
};
 
export const INITIAL_STATE: TestComponentState = {
    items: []
};
 
 

Async Actions

If you need to play with server side calls then add a configuration for those actions.

// file: services/test-service.ts
 
import { StateMachineAction } from 'react-xstate-hoc';
import { omit } from 'lodash';
import { fakeAJAX } from '../mocks/ajax';
import { TestComponentState } from '../configs/test-types';
 
export function fakeAJAX(params: Record<string, string | number | boolean>) {
    return new Promise<string[]>((resolve, reject) => setTimeout(() => {
        const rnd = Math.random();
        if (rnd > 0.5) {
            reject();
        } else {
            resolve(['ok', ...Object.keys(params)]);
        }
    }, 1000)
    );
}
 
export async function fetchData(e: StateMachineAction<TestComponentState>) {
    const params = omit(e, 'type');
    let items: string[] = [];
    try {
        items = await fakeAJAX(params);
        return { items };
    } catch (e) {
        throw new Error('Something Wrong');
    }
}
 

My Component

Let's now link the component to the state machine using withStateMachine.

// file: test-base.tsx
 
import * as React from 'react';
import { withStateMachine, StateMachineInjectedProps, StateMachineStateName } from 'react-xstate-hoc';
import { STATE_CHART, INITIAL_STATE, TestMachineEvents, TestMachineStateSchema, TestComponentState } from '../configs/test-machine';
import { fetchData } from '../services/test-service'; // described here below
import './test.css';
 
interface TestComponentProps extends StateMachineInjectedProps<TestComponentState, TestMachineStateSchema, TestMachineEvents> {
    label?: string;
}
 
export class TestBaseComponent extends React.PureComponent<TestComponentProps> {
 
     constructor(props: TestComponentProps) {
        super(props);
        const { injectMachineOptions } = props;
 
        // Injecting options from component
        injectMachineOptions({
            services: {
                FETCH_DATA: (ctx: TestComponentState, e: TestMachineEventType) => fetchData(e) //here you can link a component internal method or provide a service from props
            }
        });
    }
 
    public render() {
        const { currentState, context } = this.props;
 
        return (<div className="test">
            <h1>{currentState}</h1>
            <div>
                {this.renderChild(currentState, context)}
            </div>
        </div>);
    }
 
    private renderChild(currentStateValue: StateMachineStateName<TestMachineStateSchema>, context: TestComponentState) {
        switch (currentStateValue) {
            case 'START':
                return <button onClick={this.handleSubmit}>OK</button>;
            case 'LIST':
                return <div>
                    <div className="test-list">
                        {this.renderItems(context.items)}
                    </div>
                </div>;
            case 'ERROR':
                return <div className="test-error-box">
                    <button onClick={this.handleReset}>RESET</button>
                </div>;
            default:
                return null;
        }
    }
 
    private renderItems(items: string[]) {
        return items.map((item, i) => <div className="test-list-item" key={i}>{item}</div>);
    }
 
    private handleSubmit = () => {
        this.props.dispatch({ type: 'SUBMIT', extra: 'ok' });
    }
 
    private handleReset = () => {
        this.props.dispatch({ type: 'RESET' });
    }
}
 
export const TestComponent = withStateMachine(
    TestBaseComponent,
    STATE_CHART,
    INITIAL_STATE
);
 

Provide options to machine

See https://xstate.js.org/docs/guides/machines.html#options

You can link the machine definition action or service label to your component using injectMachineOptions. The function is available in your component props:

// TestBaseComponent class constructor
 
...
constructor(propsTestComponentProps) {
        super(props);
        const { injectMachineOptions } = props;
 
        // Injecting options from component
        injectMachineOptions({
            services: {
                FETCH_DATA: (ctx: TestComponentState, e: TestMachineEventType) => fetchData(e) //here you can link a component internal method or provide a service from props
            },
            actions: {
                ... // your code here
            }
        });
    }
...

Using enums

You can also use enums for states, actions, schema ...

import { MachineConfig, assign, log } from 'xstate';
import { StateMachineAction, MachineOptionsFix } from 'react-xstate-hoc';
 
export interface TestComponentState {
    items: string[];
    cnt: number;
}
 
export enum TestMachineState {
    START = 'START',
    PROCESSING = 'PROCESSING',
    LIST = 'LIST',
    ERROR = 'ERROR',
    SHOW_ITEM = 'SHOW_ITEM'
}
 
export enum TestMachineAction {
    SUBMIT = 'SUBMIT',
    CANCEL = 'CANCEL',
    RESET = 'RESET',
    SELECT = 'SELECT',
    EXIT = 'EXIT'
}
 
export interface TestMachineStateSchema {
    states: {
        [TestMachineState.START]: {};
        [TestMachineState.PROCESSING]: {};
        [TestMachineState.LIST]: {};
        [TestMachineState.ERROR]: {};
        [TestMachineState.SHOW_ITEM]: {};
    }
}
 
export type TestMachineEvents = 
    | { type: TestMachineAction.SUBMIT, extra: string }
    | { type: TestMachineAction.CANCEL }
    | { type: TestMachineAction.RESET }
    | { type: TestMachineAction.SELECT }
    | { type: TestMachineAction.EXIT };
 
export type TestMachineEventType = StateMachineAction<TestComponentState>;
 
export enum TestMachineService {
    FETCH_DATA = 'FETCH_DATA'
}
 
export const STATE_CHART: MachineConfig<TestComponentState, TestMachineStateSchema, TestMachineEvents> = {
    id: 'test',
    initial: TestMachineState.START,
    states: {
        [TestMachineState.START]: {
            on: {
                [TestMachineAction.SUBMIT]: {
                    target: TestMachineState.PROCESSING,
                    cond: (ctx: TestComponentState) => ctx.cnt < 10 // run N times
                }
            },
            onEntry: assign({
                items: []
            })
        },
        [TestMachineState.PROCESSING]: {
            invoke: {
                src: TestMachineService.FETCH_DATA, // see injectMachineOptions here above
                onDone: {
                    target: TestMachineState.LIST,
                    actions: assign({
                        items: (ctx: TestComponentState, e: TestMachineEventType) => {
                            return e.data.items;
                        }
                    })
                },
                onError: {
                    target: TestMachineState.ERROR,
                    actions: log((ctx: TestComponentState, e: TestMachineEventType) => e.data)
                }
            }
        },
        [TestMachineState.LIST]: {
            on: {
                [TestMachineAction.RESET]: TestMachineState.START,
                [TestMachineAction.SELECT]: TestMachineState.SHOW_ITEM
            },
            onEntry: assign({
                cnt: (ctx: TestComponentState) => ctx.cnt + 1
            })
        },
        [TestMachineState.SHOW_ITEM]: {
            on: {
                [TestMachineAction.EXIT]: TestMachineState.LIST
            }
        },
        [TestMachineState.ERROR]: {
            on: {
                [TestMachineAction.RESET]: TestMachineState.START
            }
        }
    }
};
 
export const INITIAL_STATE: TestComponentState = {
    items: [],
    cnt: 0
};

Versions

Current Tags

VersionDownloads (Last 7 Days)Tag
1.9.01latest

Version History

VersionDownloads (Last 7 Days)Published
1.9.01
1.8.51
1.8.41
1.8.31
1.8.21
1.8.11
1.8.01
1.7.21
1.7.11
1.7.01
1.6.11
1.6.01
1.5.61
1.5.51
1.5.41
1.5.30
1.5.20
1.5.10
1.5.00
1.4.00
1.3.00
1.2.20
1.2.10
1.2.00
1.1.20
1.1.00
1.0.20
1.0.10
1.0.00

Package Sidebar

Install

npm i react-xstate-hoc

Weekly Downloads

15

Version

1.9.0

License

ISC

Unpacked Size

26.2 kB

Total Files

14

Last publish

Collaborators

  • sangallimarco