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:
- redux-machine works with time-travel debugging. Time-travel debugging was the main motivation for building redux itself.
- Debugging is easy because information is in one place (the store).
- Statuses such are queryable by the user interface. This is helpful if you want to show things to the user such as loading spinners to indicate status
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.
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:
In words:
- When the status is
INITand the action type isFETCH_USERS, the machine transitions toIN_PROGRESSstatus. - When the status is
IN_PROGRESSand the action type isFETCH_USERS_RESPONSEorFETCH_USERS_FAIL, the machine transitions to theINIT(initial) status.
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.
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.
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.
See the Redux Funk Examples repo for examples using redux-machine and redux-funk:
See the Redux Saga Examples for comparison.