Skip to content

Navigation & history

Everything on IBrouter for moving around imperatively - and what <BrouterLink> adds over a plain anchor.

Navigate, and know how it went

Navigate(url) is fire-and-forget with optional replace (no new history entry), forceLoad (full page load), and historyState. NavigateAsync(url) additionally awaits the whole pipeline and reports a BrouterNavigationOutcome - so calling code can react when a guard cancelled or redirected, something Blazor's NavigationManager can't tell you.

Outcome statusMeaning
SucceededDestination rendered.
CancelledA guard/lock called Cancel() - URL unchanged.
RedirectedA guard redirected; RedirectedTo has the final URL.
NotFoundNothing matched (after NotFound handling ran).
FailedThe pipeline threw; Exception carries it.
SupersededA newer navigation started before this one finished.

History & entry state

Back() / Forward() move through history; BackAsync(delta) / ForwardAsync(delta) are awaitable and can jump several entries at once. Each entry can carry state: pass historyState: when navigating (or HistoryState on a link) and read it back from brouter.Location.HistoryState - it survives Back/Forward per entry, unlike anything keyed to the URL. Serialize to JSON for structured payloads. The router also classifies every navigation as Push, Replace, or Pop (Back/Forward) - exposed as NavigationType on guard/hook contexts and used to pick view-transition direction.

brouter.Navigate("/history-state", historyState: "draft-42");
<BrouterLink Href="/history-state" HistoryState="from-a-link">…</BrouterLink>
// later, e.g. after the user presses Back onto that entry:
var state = brouter.Location.HistoryState;

Functional query updates

Updating one query parameter shouldn't mean rebuilding the whole URL. NavigateWithQuery hands you a BrouterQueryBuilder over the current query - mutate what you need, everything else (other params, the fragment) is preserved, and the history entry is replaced by default so paging doesn't pollute Back. The builder API: Set (null removes), SetAll (multi-value, ?tag=a&tag=b), Remove, Clear, plus Get/Contains for reading.

brouter.NavigateWithQuery(q => q.Set("page", page + 1));
brouter.NavigateWithQuery(q => q.SetAll("tag", tags).Remove("page"));

Named routes & relative URLs

A Named route can be targeted without spelling its URL: NavigateToName(name, parameters) and ResolveUrl(name, parameters) fill the template (parameters that aren't in the template are appended as query pairs; enumerable values emit repeated pairs). Pair them with the generated BrouterRoutes.Names constants - see the generator page. URLs are also route-relative everywhere Brouter accepts one (links, Navigate, guard redirects): ./sibling, ../up, . and .. resolve against the current path by segment math, clamped at the root.

brouter.NavigateToName(BrouterRoutes.Names.Counter,
    new Dictionary<string, object?> { ["init"] = 7 });

<BrouterLink Href="../sibling">Up one, over one</BrouterLink>

BrouterLink

<BrouterLink> renders a real anchor - Ctrl/middle/Shift clicks keep native browser behavior; only plain left clicks are intercepted.

ParameterBehavior
HrefDestination; route-relative forms re-resolve after every navigation.
MatchActive detection: Prefix (default, segment-boundary aware) or All (exact). Root / always matches exactly.
ActiveClassClass added when active (default "active"); also sets aria-current="page".
ReplaceReplaces the current history entry instead of pushing.
PreloadIntent / Viewport / Render - see Data loading.
HistoryStateState attached to the entry created by the click.

Navigating to the URL you're already on

It does far less than it looks like it does. Blazor reuses a component whose route and parameters didn't change - the built-in Router behaves the same way - so Navigate(currentUrl) re-runs the pipeline but leaves the very instance you were trying to refresh exactly where it was. Three deliberate operations cover what people actually mean by it, none of them a navigation:

CallWhat it rebuilds
RevalidateAsync()Loader data only. Components and their state stay, content stays on screen while it refreshes. See Data loading.
ClearKeepAlive(includeActive: true)The component instances, and nothing else: no pipeline, no guards, no loaders - the rebuilt chain keeps the data it already had. See Lifecycle & keep-alive.
ReloadAsync()Both: the chain is disposed and matched again, so enter guards, RedirectTo and loaders all re-run (ctx.IsReload). No leave guard can veto it - nothing is being left.

None of the three moves the URL, the history entry or the scroll position by itself, and none fires OnNavigating/OnNavigated - the one exception is a guard that redirects during a reload, which commits a real navigation like any other redirect. The pipeline page has the step-by-step comparison.

Not-found handling

When nothing matches, the router follows NotFoundUrl (this demo's /404 page) or renders an inline <NotFound> fragment without moving the URL; the OnNotFound callback fires either way. Brouter also integrates .NET 10's NavigationManager.NotFound(): a component that discovers mid-render that its entity doesn't exist can hand control to the router's not-found pipeline while the URL stays put - try it on the outcomes page.