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.
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;
subscriptionswaits until its own chapter.
How the runtime drives it
You never call these functions. The runtime does, in a loop:
- It holds the current Model (the one mutable cell in the universe, owned by the runtime, invisible to you).
- It calls
view modeland renders the description (as DOM on the web, SwiftUI on iOS). - The user taps “+1”. The runtime does not run your handler in place; it constructs the message
Incrementand queues it. - It calls
update Increment model, gets a new Model, swaps it in. - It calls
viewagain with the new Model and reconciles the screen to match (efficiently, diffing descriptions; you never touch the DOM). - 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.