Reducers

Reducers

The generalised name for the Update function in The Elm Architecture. A reducer takes a current state and an action (or "message"), and returns a new state. Pure function. The single point of state mutation in the system.

The name comes from Array.reduce / foldl / foldr — folding a sequence into a single accumulated value. A reducer folds a sequence of actions into a sequence of states.

states = [initial_state, reduce(initial_state, msg1), reduce(reduce(initial_state, msg1), msg2), ...]

Or equivalently: current_state = msgs.fold(initial_state, reduce).

The signature

Across ecosystems, the same shape:

// Redux (TypeScript)
type Reducer<S, A> = (state: S, action: A) => S;
// In Rust (lazydap, mxr-style)
fn update(state: State, msg: Msg) -> (State, Cmd) { /* ... */ }
// Bubbletea (Go)
type Model interface {
    Update(msg tea.Msg) (Model, tea.Cmd)
}
// F# / Elmish
let update msg model : Model * Cmd<Msg> = ...
// Compose Multiplatform / MVI
fun reduce(state: State, action: Action): State

Slight syntactic variations, identical concept: (State, Action) -> State (sometimes -> (State, Effect)).

Why "reducer" caught on

Redux's Dan Abramov named it "reducer" in 2015. The name stuck partly because it described the function's mathematical shape and partly because "Update" was already overloaded in JS land.

By 2020, "reducer" was the lingua franca: React's useReducer, Vue's reducers, Bubbletea's Update method, Compose's reducers, Rx-style scan/reduce operators. People who'd never heard of Elm were using TEA-shaped reducers without knowing the lineage.

Reducer composition

Real-world apps have many slices of state. The pattern "one giant reducer" doesn't scale; you compose smaller reducers.

Two common shapes:

combineReducers (Redux-style)

Each slice has its own reducer; the top-level reducer dispatches by slice key:

const rootReducer = combineReducers({
    users: usersReducer,
    posts: postsReducer,
    ui: uiReducer,
});

Each slice reducer sees only its own state, sees all actions, ignores actions it doesn't care about.

Nested reducers (Elm-style)

The parent reducer explicitly delegates to child reducers, threading state in/out:

type Model = { user : UserModel, posts : PostsModel, ui : UiModel }

update msg model =
    case msg of
        UserMsg userMsg ->
            let
                (newUser, userCmd) = User.update userMsg model.user
            in
                ({ model | user = newUser }, Cmd.map UserMsg userCmd)

More verbose, more explicit, more flexible. Either works; pick by team preference.

In small apps (mxr's TUI, lazydap's TUI), one reducer + a giant match is simpler than composing.

Reducer + middleware

Real apps need things between dispatching an action and the reducer running: logging, persistence, dev tools, network calls, async coordination. Middleware is the seam.

Redux's middleware:

const loggingMiddleware = store => next => action => {
    console.log("dispatching:", action);
    const result = next(action);
    console.log("new state:", store.getState());
    return result;
};

Each middleware can intercept actions, transform them, dispatch additional actions, or pass through to the reducer. Composes via the curried signature.

For local apps, middleware is overkill; you can handle the same concerns in the main loop around update(). Middleware shines when the dispatch happens in many places (a sprawling React app) and you need cross-cutting concerns.

Reducers + side effects

Pure reducers can't fetch data or write files. Three patterns to handle this:

1. Cmd return (Elm-style)

Reducer returns (newState, Cmd). Runtime executes Cmd; results come back as new actions.

2. Thunks (Redux-style)

The dispatched "action" can be a function that takes (dispatch, getState). The thunk runs side effects, dispatches further actions when results arrive. Pragmatic, less pure.

3. Sagas / observables (redux-saga, redux-observable)

Background workers listen for specific actions, perform effects, dispatch new actions. More structured than thunks, more complex than Cmd.

Lazydap and Mxr use the Cmd pattern (Elm-style) because it's the simplest in Rust and the structural discipline is most useful.

Why reducers matter beyond UI

The reducer pattern generalises well beyond UI state:

Once you see the shape, you spot it everywhere. Reducers are the most general possible "state transition function." Anywhere you have state changing in response to events, reducers are the right primitive.

Common pitfalls

See also