Skip to content

FAQ & troubleshooting

The questions asked before adopting, and the symptoms hit afterwards - each with the underlying reason, because most of them turn out to be a deliberate design decision rather than a bug.

Before you adopt

Is this a replacement for the built-in Router?

Yes - a drop-in one. Point AppAssembly at your app and every @page component is discovered and matched with identical template semantics; <Found>, <NotFound> and <Navigating> port over unchanged. You can migrate without adopting a single new feature, then add guards, loaders or transitions to the pages that benefit. See the migration guide.

Can hand-declared routes and @page coexist?

That's the intended shape for most apps: keep ordinary pages colocated with their @page directives, and hand-declare a <Broute> only where a route needs behavior - a guard, a loader, keep-alive, nested children. Declaring a <Broute> with the exact template of a discovered page shadows it, which is how you attach that behavior without editing the page at all.

Which render modes and .NET versions?

net8.0, net9.0 and net10.0; Server, WebAssembly and Auto, with or without prerendering. This site is the proof - the same pages run under all three hosts. Features that need a DOM (scroll, focus, view transitions, lifecycle callbacks) are simply skipped during static prerender, and on .NET 10 an unmatched URL during static SSR produces a real HTTP 404.

Is there JavaScript to set up? Is the generator required?

No to both. The router ships its script as a static web asset - no script tag, nothing to bundle - and on browsers without the View Transitions API the animations are inert rather than broken. Bit.Brouter.Generators is entirely optional: it's an analyzer-only package that adds compile-time-safe URL builders, and everything works with plain strings without it.

What does it cost at scale?

Every route is a live component instance - roughly 3-6 KB retained each - so ~500 routes add about 2.5 MB and ~4 ms of startup. Per-navigation matching stays fast regardless, thanks to a first-segment index. Negligible for most apps, material for a very large all-@page one; the performance page has the numbers and a runnable benchmark.

Routing & matching

A URL 404s that should match

SymptomWhy, and the fix
A @page component is never matched AppAssembly isn't set, or the page lives in another assembly that isn't in AdditionalAssemblies. If it only breaks in a published trimmed build, the component was trimmed away - preserve routable components as you would for the built-in Router.
/products/list 404s but /products/en/list works Working as intended. A middle optional is required at match time - only a trailing run of optionals can be omitted. This is exact framework parity; declare a second route if you want both shapes.
A typed parameter rejects a value you expected Type constraints parse with invariant culture. {price:decimal} matches 49.99, never 49,99.
A catch-all route stopped serving your static files Add the nonfile constraint - {*path:nonfile} - so URLs that look like file names fall through instead of being routed.
The wrong route wins Specificity, not declaration order, decides: literal > complex segment > constrained parameter > parameter > wildcard > catch-all. Add a constraint to the route that should win rather than reordering.

An ambiguous-route exception at startup

Two routes match exactly the same set of URLs. Brouter throws instead of silently picking one, mirroring the built-in router's AmbiguousMatchException - usually a page copied with its @page directive, or two templates that differ only in parameter name (/users/{id} and /users/{userId} match identically). The one legal pairing is a hand-declared <Broute> deliberately shadowing a discovered @page with the same template.

A parameter arrives null

SymptomWhy, and the fix
A query value never binds [SupplyParameterFromQuery] must be paired with [Parameter] on a routed component. If the property is an enum, a Guid or a string[], the framework's supplier can't parse it - use Brouter's [BrouterQuery] instead.
A route value doesn't reach the component Binding is by name. Either rename the property to match the template, or map it explicitly with [BrouterParameter(Name = "...")]. Note that Content fragments don't bind properties at all - they receive the parameter bag as their context argument.
A parent layout sees no parameters Ancestors receive the slice of the winner's parameters that their own template declares. If the parent's template doesn't declare {id}, it won't see id - read it from the cascaded bag, or move the segment into the parent's template.

Navigation, guards & locks

A guard doesn't stop the navigation

SymptomWhy, and the fix
The guard never runs Navigate(url, forceLoad: true) skips the pipeline by design - the browser loads a new document. Guards also don't run for RevalidateAsync() or preloads, both of which are deliberately side-effect-free.
LeaveGuard doesn't fire on /edit/1 → /edit/2 The route stays matched, so nothing is being left. Implement OnRenavigatingAsync on the component - that's exactly the gap it fills.
A dialog closes itself and the navigation proceeds The lock must await the answer and then call ctx.Cancel(). Returning before the user answers approves the navigation. If the token fired, a newer navigation superseded this one - the right response is to dismiss the dialog.
Leaving the tab isn't caught No SPA router can intercept that. Arm the browser's own dialog with ConfirmExternalNavigation or SetConfirmExternalNavigationAsync(true); browser rules apply, including that its text can't be customized.
A thrown guard blocks everything By design - the pipeline fails closed rather than committing a half-authorized navigation. Catch what you can handle inside the guard and use ctx.Redirect to steer.

Data & rendering

The loader runs too often, or not often enough

SymptomWhy, and the fix
Stale data after a save A StaleTime is serving a cache hit. Call RevalidateAsync() to refresh the current page, or ClearLoaderCache() when the mutation invalidates other pages too.
The loader runs twice on first load Prerendering: once on the server, once when the app goes interactive. Enable PersistLoaderState to carry the result across, and supply LoaderStateTypeInfoResolver for trimming/AOT safety.
Cache misses where you expected hits Entries are keyed by the full URL, query included - ?page=1 and ?page=2 are separate entries. Entries also expire at LoaderCacheGcTime and evict past MaxLoaderCacheEntries.
The <Navigating> UI never appears It's revealed lazily so quick navigations don't flash it. A cache hit or a fast loader legitimately never shows it.
RevalidateAsync() appears to do nothing It's a no-op when no route in the current chain declares a Loader.
ReloadAsync() appears to do nothing A reload stands down while a navigation is in flight (that navigation is already rebuilding the page the user is heading to) and does nothing when no route is committed - a not-found fallback or an error boundary on screen. Otherwise it always rebuilds - it evicts this URL's cached results first, so a StaleTime can't quietly serve the data the discarded instances were showing.

Transitions, scroll and focus don't happen

SymptomWhy, and the fix
Nothing animates Check ViewTransitions = true first. Then: the initial load never animates by design, browsers without the API are inert, and prefers-reduced-motion swaps motion for a crossfade - which OSes report on many VMs and remote desktops even when no user asked for it.
A shared-element morph doesn't morph Both elements need the same view-transition-name, and a name must be unique on screen at any one moment - two visible elements sharing one name cancels the transition.
Custom ::view-transition-* CSS is ignored It shouldn't be - the defaults live in the bit-brouter CSS layer and unlayered author rules beat layered ones. If yours is also in a layer, the layer order decides; move it out or set ViewTransitionDefaultAnimations = false.
A #fragment link doesn't scroll The target element must exist with that id once the new page has rendered, and ScrollToFragment must be on (it is by default). A resolved fragment wins over scroll restoration and ScrollBehavior for that navigation.
Scroll and focus do nothing during prerender Expected - there is no DOM to act on yet. They run on the interactive pass.

Keep-alive and lifecycle surprises

SymptomWhy, and the fix
A hidden page keeps polling Keep-alive means still alive - timers and subscriptions keep running. Pause on OnDeactivated when Reason == Hidden and resume in OnActivated.
/item/1 and /item/2 share one instance The default KeepAliveMax is 1, which re-binds a single instance. Raise it to keep one instance per distinct parameter set, LRU-evicted beyond the budget.
Lifecycle callbacks never fire on a nested component They're discovered automatically only on the route's own Component. A descendant must take the cascaded BrouterRouteContext and call Register(this) / Unregister(this).
Memory grows as the user browses Retained pages accumulate up to their budget. Call ClearKeepAlive() on sign-out or memory pressure - and check that every OnNavigating/OnNavigated/OnError subscription is unsubscribed in Dispose.
The page keeps the previous user's / tenant's state after switching Re-navigating to the current URL can't fix it: Blazor reuses a component whose route and parameters didn't change, so the stale instance stays (the built-in Router behaves the same way). Use ClearKeepAlive(includeActive: true) to rebuild the visible page in place, or ReloadAsync() when its loaded data is stale too.

A generated URL builder is missing

The generator reads route declarations textually, so anything it can't read at compile time is skipped rather than guessed at: a dynamic Path="@expr" (and everything nested beneath it) and complex multi-part segments like /files/{name}.{ext?}. Declaring one template with two different Names raises the BRT001 warning. If nothing at all is generated, confirm the Bit.Brouter.Generators package reference is present and rebuild - the class is emitted into your project's root namespace.

Still stuck?

Most behavior questions are answered by knowing which pipeline step you're in - start with how a navigation works, then look the member up in the API reference. Every feature on this site also has a live demo you can compare your app against in the playground. Bugs and questions are welcome on GitHub.