A reducer is a single-state state machine
If you write a state machine that only has a single state, but modifies its context, what you've created is a reducer.
It's hard to state just how difficult it is to talk about state management; it all depends on the context of the conversation. The more I have been thinking about state machines, and learning about XState, the more I like the way XState talks about "application state".
In the context (🙄) of state machines:
state: which of a finite number of states the machine is currently in. e.g.
fetching
, loaded
, error
.
context: the environment in which events are taking place. This includes which user is logged in, any data that has been loaded, and errors that have been returned.
Notice how these are the same words that React uses for different things. The
state machine "context" is what you would put into a useState()
hook. In
Redux, the state machine "context" would be in your store.
Let's build the simplest possible state machine, in my new favorite language, ReScript:
The Ubiquitous Counterexample
In ReScript, our states, represented by variants, can have a payload, which is where we will place our context. In this case, our context is a simple integer.
type state = Idle(int)type event = Increment | Decrement | Resetexport let initial = Idle(0)export let transition = (state:state, event:event) =>switch (state, event) {| (Idle(value), Increment) => Idle(value + 1)| (Idle(value), Decrement) => Idle(value - 1)| (Idle(_), Reset) => initial}
Notice that we are always returning to our only state, Idle
, and our events
are only modifying our context.
What have we created here? A simple reducer. This is the equivalent, in TypeScript, of:
export const initial = 0;export function transition(state: number,event: "Increment" | "Decrement" | "Reset") {switch (event) {case "Increment":return state + 1;case "Decrement":return state - 1;case "Reset":return initial;}}
It's the kind of thing that powers Redux or what you might give to a
useReducer()
hook in React.
I can hear you rolling your eyes already.
Yeah, so what?
Well, what if we wanted to temporarily disable our counter, so that the value
couldn't be changed? You could add another disabled: boolean
to your reducer's
state
value... But if you were implementing it as a state machine, you could
have your Pause
event shift the machine into the Paused
state, where it
would ignore the Increment
and Decrement
events.
type state = Idle(int) | Paused(int)type event = Increment | Decrement | Reset | Pause | Unpauseexport let initial = Idle(0)export let transition = (. state:state, event:event) =>switch (state, event) {| (Idle(value), Increment) => Idle(value + 1)| (Idle(value), Decrement) => Idle(value - 1)| (Idle(_), Reset) => initial| (Idle(value), Pause) => Paused(value)| (Paused(value), Unpause) => Idle(value)| (Idle(_), Unpause)| (Paused(_), _) => state}
These are very, very trivially simple examples, but I think there may be a significant difference "at scale" with a real application.