1.9.0 • Published 5 years ago

react-xstate-hoc v1.9.0

Weekly downloads
2
License
ISC
Repository
github
Last release
5 years ago

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(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
            },
            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
};
1.9.0

5 years ago

1.8.5

5 years ago

1.8.4

5 years ago

1.8.3

5 years ago

1.8.2

5 years ago

1.8.1

5 years ago

1.8.0

5 years ago

1.7.2

5 years ago

1.7.1

5 years ago

1.7.0

5 years ago

1.6.1

5 years ago

1.6.0

5 years ago

1.5.6

5 years ago

1.5.5

5 years ago

1.5.4

5 years ago

1.5.3

5 years ago

1.5.2

5 years ago

1.5.1

5 years ago

1.5.0

5 years ago

1.4.0

5 years ago

1.3.0

5 years ago

1.2.2

5 years ago

1.2.1

5 years ago

1.2.0

5 years ago

1.1.2

5 years ago

1.1.0

5 years ago

1.0.2

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago