← Web interface engineering · Chapter 4

Decide where pages, routes, and state belong

Assign URL, local, and remote state by lifetime, then design reading-list routing, feature boundaries, and recoverable polling or streaming updates with scoped versions.

After this guide, you can

Implement shareable, recoverable list state and verify Back, refresh, duplicate updates, and version gaps.

A reading-list page puts search, identity, dialogs, server records, and connection state into one global store. Filters disappear on refresh, Back does not restore the view, and old messages replace newer data. These failures concern ownership and recovery rules; adding a state library does not automatically fix them.

The case is a shareable list with category, sorting, saving, and background-task progress. Classify state by lifetime and sharing, then design routes, module boundaries, and updates. The offline example checks version acceptance without creating a connection.

Place state according to its lifetime

Search, category, sorting, and shareable pagination fit the URL, allowing another reader to reconstruct the view. Dialog visibility, focus, and unsubmitted input usually belong locally. Server records remain authoritative on the backend; frontend caches are copies. Session identity has separate permission boundaries.

Avoid independent copies of records that several pages update without coordination. Define an authoritative source and synchronization path. Remote results need cache keys, invalidation, loading, and errors. A local dialog need not inherit those responsibilities.

URL parameters are untrusted inputs. Bound page numbers, allow known sorting fields, and handle unknown categories explicitly. A parameter in an application URL is not safe to interpolate into SQL or HTML.

Routing spans browser and server behavior

The History API adds or replaces history entries. Back and Forward restoration requires handling popstate through the routing mechanism. Updating history programmatically also needs a view update; do not assume pushState automatically fires the same event.

Shared links and refresh require hosting to serve the relevant paths. A working development server does not establish production route support. Avoid returning the application HTML for every missing asset, which can make image requests receive document text.

Decide whether filter changes reset pagination and how Back restores parameters. Test navigation from the entry page, direct opening, and refresh. Synchronize URL and state only when needed so they do not repeatedly write changes into each other.

Coordinate features through narrow interfaces

The list feature can own querying and saving, the task feature progress and retry, and the session feature identity. Expose required reads and operations rather than let a page mutate another feature's internal objects.

A store can implement these boundaries, but centralization alone does not define responsibility. Local state, a query cache, and limited shared context may suffice. Choose boundaries for the project's change patterns instead of creating layers that only forward calls.

Define legal task transitions, such as queued, running, then succeeded or failed. Cancellation and retry are different operations. A frontend cancellation may only stop waiting; the server must establish whether execution actually stopped.

Choose an update channel deliberately

Polling periodically obtains current state. It is simple but adds requests as frequency increases. Specify stop conditions, page-visibility behavior, and retry budgets. Stop polling after the task reaches a terminal state.

SSE provides a server-to-client event stream over HTTP, suitable for one-way progress. WebSocket supplies bidirectional messages. Choose using communication needs, proxy behavior, authentication, lifecycle, and platform capabilities, rather than the label real time alone.

Every channel still needs reconnection, duplicate handling, state recovery, and backpressure. Bidirectional messaging does not establish exactly-once business effects. Obtain a fresh snapshot after a gap or disconnection when required.

Reject older snapshots with versions

Assume the server assigns increasing versions to complete snapshots. Accepting only a version above the current one rejects duplicates and stale replacements. The model receives version 2 before version 1 and rejects the subsequent older and duplicate values.

state = {"version": 0, "value": "initial"}
def apply(version, value):
    if version <= state["version"]:
        return False
    state.update(version=version, value=value)
    return True
assert apply(2, "new")
assert not apply(1, "old")
assert not apply(2, "duplicate")
assert state["value"] == "new"
print(state)

The output is {'version': 2, 'value': 'new'}. Jumping from 0 to 2 is allowed because the input is a complete snapshot. For increments such as adding one to a counter, skipping version 1 loses an operation. Detect gaps and resynchronize rather than reuse this rule unchanged.

Versions need a comparison scope. Values from different records, tenants, or connection epochs cannot automatically be compared. Specify continuity across reconnection and server resets. The function has no network, persistence, or concurrency and does not establish channel reliability.

A build tool does not define the application

Vite provides development serving and build capabilities. It does not automatically implement authorization, production routing, or data consistency. Build output must match hosting paths, resource prefixes, and cache policy.

Investigate hosting routes and response types when refresh fails. Investigate restored URL state, cache keys, and versions when a list reverts. These boundaries help avoid misclassifying every problem as a framework failure.

Saving also needs server-rejection behavior. An optimistic update must recover or expose retry on failure, and an older failure must not undo a newer success. Define which result invalidates shared caches and verify agreement between list and detail views. A local button state is not the permanent source of saved-record truth.

Exercise: recover the list view

Assign URL parameters, local state, remote cache, and session information to owners. Select one progress channel for a synthetic task and define completion, disconnection, reconnection, duplicates, and gaps. Distinguish complete-snapshot rules from incremental-message rules.

Sharing and refresh must preserve category and sort; Back must restore the prior view; unknown parameters must be handled; older messages must not replace newer snapshots; increment gaps must trigger resynchronization; and terminal tasks must end unnecessary polling or subscriptions. Report actual route and connection checks, with the offline assertions as one mechanism check.

Sources & further reading

Sources checked on 2026-11-10. Original engineering explanation; examples are teaching data, not production measurements.