Skip to content

Routing

Router is provider-scoped state, not a global singleton. One RouterProvider owns one RouterHistory and uses Context to provide the current route context and navigation capabilities to its subtree.

let mut navigate = hooks.use_navigate();
navigate.push("/projects/boreal");

To see a complete running example first, start with Routing workspace.

RouterProvider stores history; Outlet matches Route from RouteContext and renders the matched component.

The routing runtime has three layers:

LayerLifetimeContents
RouterHistoryFollows RouterProviderHistory stack, current pointer, max history length
RouteContextCurrent history entryCurrent path, dynamic params, optional RouteState
RouteRoute table definitionPath pattern, component, child routes

Navigate writes RouterHistory. Outlet reads the current RouteContext, finds the first matching Route in routes! declaration order, and passes the remaining path to a nested Outlet.

RouterProvider(history_length: n) limits history length. The default is 10; passing 0 is treated as 1, meaning only the current latest entry is retained.

use_navigate() returns a Navigate handle. Common methods:

MethodBehavior
push(path)Add a history entry and clear route state
push_with_state(path, state)Add a history entry with one-shot state
replace(path)Replace the current history entry and clear route state
replace_with_state(path, state)Replace the current history entry with one-shot state
go(delta)Move the history pointer; negative goes back, positive goes forward
back() / forward()Semantic shortcuts for go(-1) / go(1)

Navigate writes to the RouterHistory inside the current RouterProvider, not to a global route singleton. One app can nest multiple RouterProviders, each with its own history.

let routes = routes! {
    "/" => AppShell {
        "/" => OverviewPage,
        "/projects/:slug" => ProjectDetailPage,
        "/projects" => ProjectsPage,
    },
};

A parent route component can render a fixed shell and place an Outlet inside it. The path /projects/boreal enters AppShell first, then the child Outlet matches /projects/:slug.

Static routes use prefix matching with segment boundaries; dynamic params match only one segment and do not cross /. For same-prefix routes, put the more specific dynamic route first, such as /projects/:slug before /projects.

Dynamic params come from the path:

let params = hooks.use_params();
let slug = params.get("slug");

If a page needs the matched route definition itself, use use_route():

let route = hooks.use_route();
let pattern = route.path.as_str();

Most business pages only need use_params(). use_route() is more useful for debugging, breadcrumbs, route visualizers, or your own route helper components.

RouteState comes from navigation behavior:

navigate.push_with_state(
    "/projects/boreal",
    RouteNotice { message },
);

let notice = hooks.try_use_route_state::<RouteNotice>();

try_use_route_state::<T>() returns None when there is no Router context, no state, or a type mismatch. use_route_state::<T>() is the assertion-style API and panics on absence or type mismatch.

Plain push(path) and replace(path) clear the current route state; only push_with_state and replace_with_state carry state explicitly. This prevents old state from leaking into unrelated pages when leaving a detail page.

Atom is process-wide state; Router is provider-scoped state. Storing the default Router in an Atom would let multiple RouterProviders pollute each other and would make testing, embedded subapps, and local navigation harder.

If you need externally controlled routing, prefer designing an explicit controlled Router API where the caller passes the current path and change callback. Only business state that truly needs to be shared across providers should go into Atom.