Data loading
Fetch route data before the page renders - with caching, revalidation, preloading on intent, streamed slow parts, and error boundaries when a loader throws.
Loaders
A route's Loader runs after guards pass and before the route renders; its
result cascades to the content as BrouterRouteData
(Get<T> / TryGet<T> / GetOrDefault<T>).
In a nested chain, loaders run sequentially root→leaf by default - set
ParallelLoaders on <Brouter> to run them concurrently. The
loader context tells you why it's running (IsRevalidation,
IsPreload), exposes the destination (ctx.To, including query
values), and its CancellationToken fires when a newer navigation supersedes
the load. While loaders run, the router-level <Navigating> fragment is
shown. Static route metadata takes the same path: Meta="..." cascades as
BrouterRouteMeta without running any code.
<Broute Path="/data" Loader="LoadData" StaleTime="TimeSpan.FromSeconds(30)"> private async ValueTask<object?> LoadData(BrouterNavigationContext ctx) { var page = ctx.To.GetQuery("page") ?? "1"; return await FetchAsync(page, ctx.CancellationToken); }
The stale-while-revalidate cache
StaleTime gives loader results a freshness window, keyed by the full URL (path
+ query - so ?page=1 and ?page=2 cache separately). A fresh hit
skips the loader entirely; a stale hit renders the cached data immediately and
refreshes in the background (BrouterStaleReloadMode.Background - set
Blocking to treat stale as a miss instead). Related knobs and APIs:
| API / option | Purpose |
|---|---|
| Broute.StaleTime / DefaultLoaderStaleTime | Per-route freshness window; option-level default (null = no caching). |
| LoaderCacheGcTime / MaxLoaderCacheEntries | Cache-entry lifetime (30 min) and size cap (50, oldest evicted). |
| brouter.RevalidateAsync() | Re-runs the current chain's loaders in place - not a navigation: URL unchanged, guards skipped, content stays visible, IsRevalidation set. |
| brouter.ReloadAsync() | The heavy sibling: evicts this URL's cached results, disposes the chain's components and matches again - so guards re-run and the loaders start from nothing, IsReload set. Use it when the components' own state is stale too, not just their data. |
| brouter.ClearLoaderCache() | Drops every cached loader result (e.g. on sign-out). |
| BrouterOptions.PersistLoaderState | Persists loader results across prerender→interactive so they don't run twice (AOT-safe via LoaderStateTypeInfoResolver). |
Preloading
Warm the cache before the click. BrouterLink Preload supports
Intent (hover / touch / focus, debounced by
BrouterOptions.PreloadDelay, 50 ms), Viewport (when the link
scrolls into view, once), and Render (immediately);
DefaultLinkPreload sets an app-wide default, and
brouter.PreloadAsync(url) does the same imperatively. A preload runs only the
destination's loaders - no guards, no render, failures swallowed - and marks its context
with IsPreload. The two links below use Intent: rest the pointer
on one for a moment and the 600 ms loader is already done when you click.
<BrouterLink Href="/data" Preload="BrouterLinkPreload.Intent">Data</BrouterLink> // or imperatively, e.g. after login while the dashboard is still a click away: await brouter.PreloadAsync("/data");
Deferred (streamed) data
Don't make the whole page wait for its slowest query. A loader can await only the critical
data and return the slow part as an unawaited Task: the route reveals
immediately, and <BrouterAwait> streams the result in when it lands -
with <Pending>, <Resolved>, and
<Error> fragments (a cancelled task surfaces as
TaskCanceledException).
<BrouterAwait Task="@Report.SlowDetails"> <Pending>Crunching the numbers…</Pending> <Resolved Context="details"> @* render details *@ </Resolved> <Error Context="ex">Details unavailable: @ex.Message</Error> </BrouterAwait>
Error boundaries
When a loader (or lifecycle callback) throws, the router walks from the failed route up its
ancestors to the nearest <ErrorContent>; the router-level
ErrorContent is the last resort, and ancestor layouts keep rendering either
way. The boundary receives a BrouterErrorContext with the
Exception, the target Location, and RetryAsync(),
which re-runs the full navigation pipeline in place. The global IBrouter.OnError
hook fires for these failures too - boundaries handle display, the hook handles logging.