Skip to content

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 / optionPurpose
Broute.StaleTime / DefaultLoaderStaleTimePer-route freshness window; option-level default (null = no caching).
LoaderCacheGcTime / MaxLoaderCacheEntriesCache-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.PersistLoaderStatePersists 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.