stack-fsm-reducer v1.1.4
stack-fsm-reducer
This is simple implementation of stack based finite state machine.
#Instalation using npm:
npm i stack-fsm-reducer --saveor using yarn:
yarn add stack-fsm-reducer#Motivation
Classic FSM allows you only to transition between states without keeping track of previous states. This reducer can be used to implement the wizard dialog. Let say you have to fill out some data on each stage, and you are not allowed to go to the next form until you finish filling out all fields on current form. Such mechanism can be perfectly described using Stack FSM.
#Example
Let say we are trying to do a RPG game and we need player to define a race of his character and class, not all races can be mages so we need to be sure that we are not presenting to the user wrong options.
const state = {
raceId: undefined,
classId: undefined
}Now we want to:
- Enter
Setupstate - Ask user to fill in
raceId, stay in this state until he finishes the job - Pass the raceId to the next state
- Ask user to select
classId, stay in this state until he finishes selection. - Pass both
raceIdandclassIdto the next state
Because each state must know to which state transition next it might be difficult to write reusable composable code.
We can do better. Imagine that we could somehow remember from which state we are coming and return to that state with the data retrieved from the user. This can be achieved using Stack FSM, a natural evolution of FSM.
Instead of just switching to a new state we will push the state to a stack.
So we will do:
- Push
Setupstate to stack which will contain our properties to fill out. - If
raceIdis undefined, pushQueryInputstate to stack initialized with some query data. - Stay in
QueryInputuntil user sendsonResponseaction with selected data. - If user sends valid
onResponseaction pop current state from stack going back toSetupstate filling outraceIdin that state - If
raceIdis not undefined andclassIdis undefined, push once againQueryInputto the stack filling out the state with the query data - Stay in
QueryInputuntil user sendsonResponseaction with selected data. - If user sends valid
onResponseaction pop current state from stack going back toSetupstate filling outclassIdin that state - Finally if both
raceIdandclassIdis not undefined in we can replace current state withGamestate, filling that state withraceIdandclassId
Notice how QueryInput can be reused in this example.
Create stack FSM reducer:
import {createStackReducer, head, push, pop, splitLastTwo} from 'stack-fsm-reducer'
const stackFSMReducer = createStackReducer(mapOfStates)Define map of states to reducers. Notice:
- Initial state is just empty string
- Each state must have stateId property
- If you want to match exactly stack with 2 states you need to separate state ids with /
const mapOfStates = {
'':(state)=>push(state, {stateId: 'setup', raceId:undefined, classId:undefined}),
'setup':setupReducer,
'setup/queryInput':queryInputReducer,
'game':gameReducer
}Define state reducers:
const setupReducer = (state, action) => {
const setupState = head(state)
if(setupState.raceId === undefined){
return push(state, {stateId: 'queryInput', query: 'Please enter raceId', options:['human', 'dwarf', 'orc'], writeResponseTo: 'raceId'})
} else if(setupState.classId === undefined){
let classOptions
if(setupState.raceId === 'orc'){
classOptions = ['warrior', 'warlock']
} else if(setupState.raceId === 'human'){
classOptions = ['warrior', 'paladin' ,'mage']
} else {
classOptions = ['warrior', 'paladin']
}
return push(state, {stateId: 'queryInput', query: 'Please enter classId', options:classOptions, writeResponseTo: 'classId'})
} else if(setupState.raceId && setupState.classId) {
return push(pop(state), {stateId:'game', raceId: setupState.raceId, classId:setupState.classId })
}
}
const queryInputReducer = (state, action) => {
switch(action.type){
case 'onResponse': {
const [tail, prevState, queryInputState] = splitLastTwo(stack)
const newPrevState = {
...prevState,
[queryInputState.writeResponseTo]: queryInputState.options[action.optionId]
}
return [...tail, newPrevState]
}
default: return state;
}
}
const gameReducer = (state, action) => {
//implementation of the game
}Notice couple of things, stackFSMReducer expects state to be JavaScript array, This library comes with selection of useful functions to manipulate the stack, but since stack is just an array you can use VanillaJS to work with it. You can create infinite loop, be careful with the definition of state transitions.
Inside map of states you can use minimatch like for example:
const mapOfStates = {
'**/setup':setupReducer,
'**/queryInput':queryInputReducer,
'**/game':gameReducer
}This way you can reuse your reducers no matter the state of the stack.
#License
MIT