Show Navigation Progress
Goal: show a progress indicator while a lazy route resolves its chunk, so a slow network is visible instead of feeling frozen.
When you navigate to a route whose component (or a layout in its branch) is Router.lazy, the router is deferred-commit: it resolves the chunk before swapping the URL, keeping the previous page mounted meanwhile. That resolve window is exposed as a reactive signal, Router.navigating, that you read to render pending UI.
import { Component, h } from "@weftui/core";
import { Router } from "@weftui/router";
import { Stream } from "effect";
const Shell = Component.gen(function* () {
const outlet = yield* Router.Outlet;
const nav = yield* Router.navigatingStream;
return yield* h.div({ id: "app" }, [
h.div({
id: "nav-progress",
"aria-hidden": "true",
class: Stream.map(nav.changes, (s) =>
s._tag === "Navigating" ? "nav-progress is-navigating" : "nav-progress",
),
}),
h.main([outlet]),
]);
});Thread the signal into a persistent layout (the outermost Shell is ideal, since it never re-renders across navigations), and style the pending class however you like — a top bar, a cursor change, a dimmed outlet.
The signal
NavState is a two-state machine:
type NavState = { readonly _tag: "Idle" } | { readonly _tag: "Navigating"; readonly to: string };Read it two ways, mirroring Router.params / Router.paramsStream:
Router.navigating— theSubscribable<NavState>on theRouterservice.Router.navigatingStream— anEffectresolving thatSubscribable, for use in aComponent.genbody (as above).
The to field on Navigating is the target URL, if you want to label where the app is going.
Behavior to expect
- Only lazy navigations flip it. A navigation whose matched branch has no
Router.lazynode commits synchronously andnavigatingstaysIdle— an entirely eager app never seesNavigating, and adding the reader costs nothing. - Latest-wins. Rapid successive navigations commit only the newest; a superseded navigation never resets the signal (the newer one owns it).
- Back/forward.
popstateinto a lazy route also resolves before committing, so the indicator shows for browser back/forward too. - Failure resets it. A rejected chunk load is a defect that resets
navigatingtoIdle(it never sticks on), then surfaces through normal defect handling. - Server renders
Idle. Server render is buffered, sonavigatingis a client-only concern; the server supplies a constantIdleso the sameShelltype-checks and renders on both sides.
See also
Router.navigatingAPI reference- Split Routes Lazily — the
Router.lazydeferred-commit navigation this reports on - examples/router-ssr — wires this exact progress bar in its
Shell(components/shell.ts), with apending-navigation.browser.test.ts