Dataslope logoDataslope

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:

  1. 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.
  2. Transitions are pure functions. Instead of state.change() you write next = 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

Code Block
TypeScript 5.7

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.

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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".

Code Block
TypeScript 5.7

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

Challenge
TypeScript 5.7
Write a pure cart reducer

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 } ]
[]

QuestionSelect one

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

On this page