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
| Symptom | Why, 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
| Symptom | Why, 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
| Symptom | Why, 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
| Symptom | Why, 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
| Symptom | Why, 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
| Symptom | Why, 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.