The Elm Architecture (TEA)
The Elm Architecture (TEA)
Synthesis. The pattern, the lineage, the "how to think about it" — pulling together Elm (the language), The Elm Architecture, Pure Functions and Immutable State, Side Effects via Cmd, Reducers, React's Evolution Toward Elm, and MVU in Other Ecosystems into one place.
The 30-second pitch
Your entire app's state is one immutable value (Model). Things that happen are Msgs. A pure function update(state, msg) -> (new_state, cmd) is the only place state changes. A pure function view(state) -> ui renders. Side effects are returned as Cmd values; the runtime executes them. Cycle: state → view → user does something → msg → update → new state → view.
That's it. The discipline is everything.
Why it works
Three guarantees fall out of the constraints:
- Predictable. Same state always renders the same view. Same state + same msg always produces the same new state. No race conditions in user code.
- Testable.
updateis(input, input) -> output. No mocks. Pass values, assert values. - Replayable. Recorded sequences of Msgs reproduce the entire app history. Time-travel debugging is trivial.
The constraints are simple to state and powerful in practice. That's what makes the pattern survive: it's hard to "improve" without breaking the guarantees.
The lineage
1990s Functional UI patterns in Haskell, OCaml, Lisp
↓
2003 The "Functional Reactive Programming" research wave
↓
2012 Evan Czaplicki creates Elm — combines FRP + Haskell-like syntax + browser
↓
2014 Czaplicki refines: simpler runtime, "The Elm Architecture" pattern emerges
↓
2015 Dan Abramov releases Redux at React Europe — Elm Architecture in JS
"Live React: Hot Reloading with Time Travel" demo
↓
2018 React Hooks (useState, useReducer, useEffect) bake Elm-shaped patterns into React core
↓
2018 Bubbletea (Go) — explicit Elm Architecture for terminal apps
↓
2019 tui-realm (Rust) — Elm Architecture on top of tui-rs/ratatui
↓
2019 JetBrains Compose — declarative UI for Kotlin, MVI patterns common
↓
2020+ Compose Multiplatform, SwiftUI, Flutter (BLoC/Riverpod), Vue (Pinia) — all Elm-flavoured
↓
2024 Lazydap adopts hand-rolled Elm Architecture in Rust because the discipline
scales while keeping the rust+ratatui+DAP+tokio learning curve manageable.
Elm-the-language is mostly historical. Elm-the-architecture is mainstream.
The five concepts you need
| Concept | Atomic note | One-line summary |
|---|---|---|
| Model | (no separate note; covered in The Elm Architecture) | Your entire app state, as plain immutable data |
| Update / Reduce | Reducers | Pure function (State, Msg) -> (State, Cmd); only place state changes |
| View | The Elm Architecture | Pure function State -> UI; no mutation, no I/O |
| Cmd | Side Effects via Cmd | Description of a side effect; runtime executes; result comes back as Msg |
| Sub | Side Effects via Cmd | Subscription to external events (timers, websockets, IPC) |
That's the whole architecture in five things.
When TEA shines
- UI apps of any kind. Web (Elm, Redux). Desktop (SwiftUI, Compose). Mobile (Flutter+BLoC, Riverpod). Terminal (Bubbletea, tui-realm, hand-rolled in Rust).
- State machines that are too complex for ad-hoc code but not complex enough to need a formal SM library. The reducer IS the state machine.
- Multi-event systems where many event sources (UI, network, timers, IPC) need to coordinate around a coherent state. The single update funnel keeps it sane.
- Game loops where state needs to be deterministic for replay/debugging.
- Distributed coordination — Raft, CRDTs, replicated state machines all benefit from "deterministic update function."
When TEA hurts
- Trivial throwaway apps. "Hello, world" in TEA is more code than mutable JS. For a 50-line script, TEA is overkill.
- Hot loops with high state churn (per-frame physics, video processing). Allocating new state objects per frame is wasteful. Use mutable buffers in those layers; isolate them.
- Bidirectional state sharing. When state is owned by the parent but children need to mutate it, TEA's "lift state up" pattern can become awkward. Real frameworks have escape hatches (Context, dependency injection, refs).
- Imperative GPU/IO/DOM APIs that don't fit "pure render of state." Wrap them in
view, but they break referential transparency.
For 90% of "I need a UI" / "I need state coordination" cases in apps the size of mxr/lazydap, TEA is the right shape.
How Lazydap uses it
crates/tui/ is hand-rolled TEA in Rust:
pub struct AppState { /* model */ }
pub enum Msg { Key(KeyEvent), DapEventEvent, Tick, /* ... */ }
pub enum Cmd { Quit, SendIpc(Request), LoadSource(PathBuf), None }
pub fn update(state: AppState, msg: Msg) -> (AppState, Cmd) {
match msg {
Msg::Key(KeyEvent { code: KeyCode::F(5), .. }) =>
(state, Cmd::SendIpcContinue { .. }),
Msg::DapEventStopped { .. } =>
(state.with_paused(...), Cmd::None),
_ => (state, Cmd::None),
}
}
pub fn view(frame: &mut Frame, state: &AppState) { /* render */ }
Main loop:
loop {
terminal.draw(|f| view(f, &state))?;
let msg = tokio::select! {
Some(m) = input_rx.recv() => m,
Some(m) = ipc_rx.recv() => m,
_ = tick.tick() => Msg::Tick,
};
let (new_state, cmd) = update(state, msg);
state = new_state;
perform(cmd).await;
}
~50 lines of boilerplate around update and view. No framework. The discipline is the framework.
Mxr's TUI uses a similar pattern in spirit, with Bubbletea-influenced specifics.
What to remember
- TEA is one of the few patterns that's universally adopted under many names.
- "Reducer" = "Update" = "Reduce" = "MVU's update" = same function.
- React + hooks + useReducer ≈ TEA with weaker purity guarantees.
- Bubbletea, tui-realm, SwiftUI, Compose, Flutter+BLoC, Vue+Pinia — all variants.
- The architecture is portable; the language is incidental.
- For a learning project, hand-roll it before reaching for a framework. The discipline IS the value.
Reading order if you want to go deeper
- Elm (the language) — see the language that crystallised the pattern (1 hour with the official guide is the fastest internalisation).
- The Elm Architecture — the canonical shape.
- Pure Functions and Immutable State — the foundations.
- Side Effects via Cmd — the bit that surprises people.
- Reducers — the generalised vocabulary.
- React's Evolution Toward Elm — if you're a React person, this gives you the lineage.
- MVU in Other Ecosystems — once you spot the pattern, you spot it everywhere.
Other syntheses (sibling MOCs in the vault)
The other "How X actually works" synthesis notes — each is the entry point to its own cluster:
- How Debuggers Actually Work — DAP / adapters / ptrace / DWARF / breakpoints
- How Email Actually Works — SMTP / IMAP / MIME / threading / OAuth / internal model
- How Daemons Work — daemon lifecycle, PID files, signals, auto-spawning
- How Processes Talk to Each Other — IPC, Unix sockets, framing, JSON-RPC
- Client-Agnostic Cores — headless core + many clients (TEA is a common state-mgmt choice for the client layer)
Further reading
- The Elm Guide: https://guide.elm-lang.org/architecture/
- Dan Abramov's "Live React: Hot Reloading with Time Travel" (2015): https://www.youtube.com/watch?v=xsSnOQynTHs
- Evan Czaplicki's "Let's be mainstream! User-focused design in Elm": https://www.youtube.com/watch?v=oYk8CKH7OhE
- Bubbletea README: https://github.com/charmbracelet/bubbletea
- The Composable Architecture (TCA) for Swift: https://github.com/pointfreeco/swift-composable-architecture
- Redux's "Three Principles": https://redux.js.org/understanding/thinking-in-redux/three-principles