Functional State Management
Reducers, immutable transitions, the State monad sketch, and how to model time-travel and undo as data.
"Functional" and "state" sound like opposites — but the functional approach to state is actually one of the field's greatest hits. Two ideas underpin it:
- State is just a value. A "state" is whatever data you currently have. It is not a hidden, mutable box; it is a plain object that flows through the program.
- Transitions are pure functions. Instead of
state.change()you writenext = transition(state, event). The old state and the new state coexist; nothing was mutated.
If those two ideas sound familiar — they're the foundation of
Redux, React's useReducer, Elm's update function, and pretty
much every modern state library.
The reducer pattern
reducer is a pure function. play is a reduce over actions —
which means our entire state lifecycle is just a fold. That's
how you can think of state functionally:
The current state =
reduce(reducer, initial, history).
Free time-travel
Once reducer is pure and history is just an array of actions,
you get "time travel" for free: prefix of history → state at any
point in time.
You can scrub the timeline forward and backward without recomputing anything that hasn't changed. You can also persist the history, replay it on another machine, or save it for debugging.
Modeling undo / redo
Undo/redo is a state machine over stacks of states. As a pure data structure:
Nothing is mutated. Each previous Editor snapshot is still alive
in memory (cheap, because of structural sharing on the
not-actually-changed parts). Undo and redo are just "shift a
value from one stack to the other".
This is the same pattern Photoshop, Git, VSCode, and every serious text editor use internally.
The State<S, A> sketch (briefly)
Sometimes you want to thread a state through a sequence of pure
computations without making the state a global. That's modeled in
FP by State<S, A> = (s: S) => [A, S] — "given the current state,
produce a value and the next state".
The point isn't that you should write a State monad in production
TypeScript every day — it's to see that mutable state isn't
required. The same effect is achievable with a function that
returns both an answer and the next state, composed with chain.
Why this matters
Treating state as a value gives you:
- Predictability: every transition is a function. Same inputs, same output.
- Inspectability: the state and the action are both just data you can log, persist, replay.
- Testability: test transitions like ordinary functions.
- Time travel & undo: essentially free, because old states weren't destroyed.
- Concurrency safety: immutable values can be shared between threads, workers, requests, components without locking.
The diagram is the model: time as a stream of pure transitions, the history as the source of truth.
A small challenge
Implement reduce(state, action) in
cart.ts for these actions:
{ kind: "add"; sku: string; qty: number }— add (or merge) an item; if SKU exists, increase its qty{ kind: "remove"; sku: string }— drop the matching item{ kind: "clear" }— empty the cart
The reducer must be pure — return a new State; do not mutate
state or state.items.
Expected output
[ { sku: 'A', qty: 1 } ]
[ { sku: 'A', qty: 3 } ]
[ { sku: 'A', qty: 3 }, { sku: 'B', qty: 1 } ]
[ { sku: 'A', qty: 3 } ]
[]
Why does the reducer pattern ((state, action) => newState) make state management easier to reason about?
Because reducers can be written without TypeScript
Because each transition is a pure function — same inputs give the same output — so state changes are predictable, replayable, and trivially testable
Because reducers can mutate the state in place for performance
Because actions are usually classes