Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Loop

Every interactive program answers three questions: what is the state, how is it shown, and how does it change? MVU answers each with one piece, and connects them in a single one-way loop.

The MVU loop

A complete page

Here is a counter, whole. Every Mar page you ever write is this shape, scaled up:

module Frontend.Counter exposing (page)


import UI exposing (navigationStack, navigationTitle, section, list, title, button)


type alias Model =
    { count : Int }


type Msg
    = Increment
    | Decrement


init : (Model, Cmd Msg)
init =
    ( { count = 0 }, Cmd.none )


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        Increment ->
            ( { model | count = model.count + 1 }, Cmd.none )

        Decrement ->
            ( { model | count = model.count - 1 }, Cmd.none )


view : Model -> View Msg
view model =
    navigationStack [ navigationTitle "Counter" ]
        [ list []
            [ section []
                [ title (String.fromInt model.count)
                , button [] Increment "+1"
                , button [] Decrement "-1"
                ]
            ]
        ]


page : Page
page =
    Page.create
        { path = "/"
        , title = "Counter"
        , init = init
        , update = update
        , view = view
        , subscriptions = always Sub.none
        }

Walk the pieces:

  • Model is all the state of this page, as one immutable record. Not state scattered across components; one value you could print.
  • Msg is a union of everything that can happen. Two things can happen to a counter. A real page might have fifteen. The type is a complete, compiler-checked inventory of the page’s events.
  • init is the starting state (plus any commands to run immediately, such as fetching data).
  • update is the entire behavior of the page: given a thing that happened and the current state, produce the next state. Pure, exhaustive, no exceptions possible.
  • view is the entire appearance of the page: given the state, describe the screen. Also pure: same Model, same screen, every time.
  • page packages them with a route; subscriptions waits until its own chapter.

How the runtime drives it

You never call these functions. The runtime does, in a loop:

  1. It holds the current Model (the one mutable cell in the universe, owned by the runtime, invisible to you).
  2. It calls view model and renders the description (as DOM on the web, SwiftUI on iOS).
  3. The user taps “+1”. The runtime does not run your handler in place; it constructs the message Increment and queues it.
  4. It calls update Increment model, gets a new Model, swaps it in.
  5. It calls view again with the new Model and reconciles the screen to match (efficiently, diffing descriptions; you never touch the DOM).
  6. Repeat forever.

Everything flows one way around the circle: state to screen to input to message to state. There are no other paths. The screen cannot write to the state; input cannot mutate the view; nothing happens between frames that update did not decide.

Why one-way flow matters

Interactive UI was traditionally written as a web of observers: this input’s change handler updates that label, unless the other checkbox is on, in which case… Each widget holds a shard of state, and consistency between shards is maintained by hand, by remembering every path. The bug report reads “if you click these in the wrong order, the totals disagree”.

MVU makes disagreement impossible by construction. There is one state, and every pixel is a function of it. Two parts of the screen cannot contradict each other, because neither owns anything: both are derived, every frame, from the same Model. “Sync the UI” stops being your job; it is the loop.

The other gift is explainability. The Model at any moment equals init folded through the exact sequence of messages so far. That sequence is finite, typed, and loggable. Mar’s dev tools record it, which is why the time-travel debugger can replay your session: your app is, mathematically, a left fold over its events. (A fancier way to say it: MVU makes event sourcing the substrate of the UI.)

“Where are the components?”

Newcomers from React or Vue ask this within the hour. The honest answer: there are none, and it is on purpose. Reusable view logic is just functions (a taskRow : Task -> View Msg used by three pages is an ordinary function). Reusable state logic mostly dissolves once state is one value and behavior is one function; where it persists, you extract helper functions over Model and Msg.

What you do not get is a tree of stateful components, each with private lifecycles to coordinate. That tree is precisely the thing MVU exists to not have. A page is the unit of state; below it, everything is a pure function.

The counter never talks to a server, though, and real pages do. That requires the second half of update’s return value, and the next chapter.