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:
- 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
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:
In words:
- When the status is
INIT
and the action type isFETCH_USERS
, the machine transitions toIN_PROGRESS
status. - When the status is
IN_PROGRESS
and the action type isFETCH_USERS_RESPONSE
orFETCH_USERS_FAIL
, the machine transitions to theINIT
(initial) status.
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.