mheiber / redux-machine

A tiny library (12 lines) for creating state machines in Redux apps
336 stars 16 forks source link
javascript reducer redux state-machine state-management

redux-machine

redux-machine

A tiny lib (12 lines) for creating state machines as swappable Redux reducers

If you are using Immutable JS in your stores, see redux-machine-immutable.

redux-machine enables you to create reducers that can transition between different "statuses." These are likes states in a finite state machine. The goal is for redux-machine to support complex workflows simply while keeping all state in the redux store. Keeping all state in the store is good because:

Install

npm install redux-machine --save

redux-machine internally uses Object.assign, which is an ES2015 feature. If you need to support older browsers, you can use a polyfill such as core-js.

How to Use

This is the entire API for redux-machine:

// entire API, no middleware required
import { createMachine } = from './index.js'

const fetchUsersReducer = createMachine({
    'INIT': initReducer,
    'IN_PROGRESS': inProgressReducer
})

The reducer returned by createMachine will act like initReducer when its status is INIT and will act like inProgressReducer when the status is IN_PROGRESS. If the store's state.status is undefined, the reducer for INIT is used (so it's a good idea to provide a reducer for the INIT status).

initReducer and inProgressReducer can do status transitions by setting state.status:

const initReducer = (state = {error: null, users: []}, action) => {
    switch (action.type) {
    case 'FETCH_USERS':
        return Object.assign({}, state, {
            error: null,
            // transition to a different status!
            status: 'IN_PROGRESS'
    })
    default:
        return state
    }
}

const inProgressReducer = (state = {}, action) => {
    switch (action.type) {
    case 'FETCH_USERS_RESPONSE':
        return Object.assign({}, state, {
            error: null,
            users: action.payload.users,
            // transition to a different status!
            status: 'INIT'
        })
    case 'FETCH_USERS_FAIL':
        return Object.assign({}, state, {
            error: action.payload.error,
            // transition to a different status!
            status: 'INIT'
        })
    default:
        return state
    }
}

The example above defines the following state machine:

status machine for the api-calling example

In words:

Making Finite State Machine Reducers without a Library

You don't need redux-machine, since you can accomplish almost the same thing as in the example above by defining fetchUsersReducer as follows:

const fetchUsersReducer = (state, action) => {
    switch (state.status) {
    case 'INIT':
        return initReducer(state, action)
    case 'IN_PROGRESS':
        return inProgressReducer(state, action)
    default:
        return initReducer(state, action)
    }
}

The (marginal) advantages of using redux-machine over just using the FSM pattern is that you can more clearly express intent and write slightly less code.

Supporting an Extra Argument

redux-machine supports to passing an extra argument to state reducers, for cases where a state reducer requires a third argument for other state it depends on.

Asynchronous Effects

redux-machine doesn't prescribe a way of handling asynchronous effects such as API calls. This leaves it open for you to use no async effects library, redux-loop, redux-thunk, redux-saga, redux-funk or anything else.

Examples

See the Redux Funk Examples repo for examples using redux-machine and redux-funk:

See the Redux Saga Examples for comparison.