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

449. A page follows the value that describes it

Date: 2026-09-20

Status

Accepted.

Context

A web-view loaded whatever its WebPage named when it opened, and then nothing could change it. An address bar — a field, a Go button, a page that follows — was not expressible, and neither was the ordinary case behind it: an application that shows a document chosen somewhere else on the screen.

The obvious answer is a handle to call navigate(url) on. The toolkit’s own rule points elsewhere: a widget is a value rebuilt every frame, and the widget already takes the page as a value. Nothing was reading it after the first frame.

Decision

The widget follows its page value. The application holds the page it wants, rebuilds, and web-view navigates:

private WebPage showing = WebPage.of(START);
// ...
new WebView(showing)
// and the Go button is:
setState(() -> showing = WebPage.of(typed));

No new API, no handle, nothing to hold on to, and no second way of saying what the page shows that could disagree with the first.

Compared by what it shows, not by equals

WebPage.showsSameAs compares the url and the document and deliberately nothing else.

It has to. ADR-0448 put callbacks on the page value, a callback is a lambda, and lambdas have no value equality — so two rebuilds of the same on(...) expression are unequal pages and a widget navigating on equals would reload for ever. Even without them the right comparison is this one: a title, a window size and the inspector are things about a page that do not change what is on it, and re-navigating to put a different word in a titlebar is a reload nobody asked for.

The spinner comes back, and only for this

ADR-0445 latched shown after the first load, because a page that re-parked on every navigation would blank itself when a site redirected — which is what a real run showed GitHub doing six seconds in. A browser keeps the old document up until the new one commits.

An application saying “show this instead” is a different event, and the latch is released for it. What is on screen is no longer what was asked for, and several seconds of stale content with no sign of life is worse than a spinner. That distinction is the whole reason navigation is driven from the value: the widget can tell the two apart because one of them arrives as a rebuild and the other does not.

Alternatives considered

An imperative navigate(url) handle. Direct, familiar, and it adds a second source of truth: after page.navigate(b) the widget’s own value still says a, and the next rebuild for any unrelated reason would navigate back. Making that safe means the handle writing into the widget’s state, which is the declarative design with extra steps.

Comparing with equals and telling applications not to use lambdas in a page that may be rebuilt. A rule nobody would remember, enforced by an infinite reload.

Re-navigating on any page change including the title. Simpler to describe and wrong: it reloads a document to change a window title.

Consequences

An address bar is fifteen lines of application code, and the showcase has one. Typing changes a field; pressing Go changes the page; the widget does the rest.

WebPage grows showsSameAs, which is a method that exists because equals cannot be used — worth saying plainly, since a reader will otherwise reach for equals first.

A page can be swapped for a document and back, because showsSameAs compares the html as well as the url.

Navigation is one frame late, like everything else web-view does from its painter. Nobody can see it.

Nothing observes the result. The application knows what it asked for and not whether it arrived: a url that 404s or never resolves looks the same from outside as one that worked, because the widget keeps the load state to itself. An application wanting to show “could not load that” needs something this decision does not provide, and the state is already there to expose when somebody needs it.