stack-fsm-reducer
v1.1.4
Published
Stack finite state machine reducer
Downloads
6
Maintainers
Readme
stack-fsm-reducer
This is simple implementation of stack based finite state machine.
#Instalation using npm:
npm i stack-fsm-reducer --save
or 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
Setup
state - 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
raceId
andclassId
to 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
Setup
state to stack which will contain our properties to fill out. - If
raceId
is undefined, pushQueryInput
state to stack initialized with some query data. - Stay in
QueryInput
until user sendsonResponse
action with selected data. - If user sends valid
onResponse
action pop current state from stack going back toSetup
state filling outraceId
in that state - If
raceId
is not undefined andclassId
is undefined, push once againQueryInput
to the stack filling out the state with the query data - Stay in
QueryInput
until user sendsonResponse
action with selected data. - If user sends valid
onResponse
action pop current state from stack going back toSetup
state filling outclassId
in that state - Finally if both
raceId
andclassId
is not undefined in we can replace current state withGame
state, filling that state withraceId
andclassId
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