Choosing Elm’s The Elm Architecture for Predictable Front‑End State
See how Elm’s Model‑View‑Update pattern eliminates runtime surprises and gives you a clear path to testable, maintainable UI code.
24 Jul 2026, 12:19 UTC

Problem: Unpredictable UI state in a growing SPA
As a front‑end codebase expands, manual DOM updates and scattered event handlers make it easy to introduce states that the UI never actually reaches. Debugging becomes a game of reproducing user actions, and adding a new feature often means touching several files to keep the view in sync with the underlying data.
Thesis: Elm’s The Elm Architecture gives you a compile‑time guarantee of predictable state transitions
By enforcing a strict separation of concerns—Model (application state), View (pure HTML function), and Update (message‑handling function)—Elm ensures that every state change is explicit, type‑checked, and free of runtime exceptions. Side effects are confined to ports or Cmd/Sub, keeping pure functions testable in isolation.
How TEA works
The core loop is:
- Model – a plain immutable record that holds all state needed for the UI.
- View – a function
Model -> Html Msgthat describes what the user sees; it never mutates the Model. - Update – a function
Update Msg Model -> (Model, Cmd Msg)that receives a message, returns a new Model, and optionally issues commands (e.g., HTTP requests).
Because the View is a pure function of the Model, the runtime simply re‑renders the View whenever the Model changes. No manual DOM manipulation is required, and the compiler guarantees that all possible messages are handled.
Worked example: a counter component
Below is a minimal Elm program that implements a counter with increment and decrement buttons. Save it as Main.elm in a fresh project folder.
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MODEL
type alias Model = {
count : Int
}
init : Model
init = {
count = 0
}
-- UPDATE
type Msg
= Increment
| Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment -> { model | count = model.count + 1 }
Decrement -> { model | count = model.count - 1 }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
-- MAIN
main : Program () Model Msg
main =
Browser.sandbox { init = init, view = view, update = update }
To build and run this example:
elm init– createselm.jsonand thesrcdirectory (run in your project folder; no special permissions needed).- Move
Main.elmintosrc/. elm make src/Main.elm --output=main.js– compiles the Elm code to JavaScript.- Open the generated
main.jsin a simple HTML page or runelm reactorand navigate tohttp://localhost:8000/src/Main.elmto see the counter in action.
When you click the buttons, the update function is the only place where the Model.count value can change. The view function reacts automatically; there is no need to call setAttribute or similar DOM APIs.
Trade‑off: Learning curve and JavaScript interop
Developers used to mutable state or two‑way binding must adjust to explicit message passing and pure functions. This can feel verbose at first, especially when modeling complex forms or animations. Additionally, integrating with existing JavaScript libraries requires ports or custom elements, which adds a thin boilerplate layer but keeps the Elm core pure.
You can verify that the interop boundary is respected by checking that any call to a port appears only in the Cmd or Sub part of update and that the port definition in elm.json lists the expected JSON types.
Actionable closing: start small, test pure logic
Add a new feature as a separate Elm module that exposes its own init, update, view, and subscriptions. Write a unit test with elm-test that asserts the resulting Model after a sequence of messages—no mocks needed because the update function is pure. If the test passes, you have confidence that the module’s state transitions are correct, and you can compose it into the larger application knowing the compiler will catch any mismatched messages or missing cases.
By beginning with a contained piece like the counter above, you experience the guarantees of TEA firsthand and can decide whether the benefits outweigh the initial adjustment period for your team.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.