Router
Router is Fission's basic path-matching widget.
A beginner-friendly way to think about it is this: the active shell exposes a logical path such as /inbox or /projects/42, your route reducer stores that path, and Router decides which screen widget to return. Router is the default matcher, not a required navigation authority: applications and third-party libraries may consume ShellRouteChanged and render routes themselves.
That design is important for cross-platform work. Link, NavigationCommand, RouteLocation, and ShellRouteChanged are shell-neutral. The Web shell maps them to pathname or hash URLs and browser history; graphical and Terminal shells maintain their own logical history. The Router only matches the resulting string and builds the corresponding screen.
Example
use fission::prelude::*;
let router: Widget = Router::<AppState>::new()
.with_path(view.state().current_path.clone())
.route_component("/projects", ProjectList)
.route_component("/settings", SettingsScreen)
.not_found(|| Text::new("Page not found"))
.into();
route_component stores a concrete component and converts it only after its
path matches. The existing closure-based route and parameter-aware
route_builder APIs remain available.
Protected routes
Keep access checks at the route table rather than repeating them inside every
protected screen. Derive a RouteDecision from application state and wrap the
route component with ProtectedRoute:
let account_access = match &view.state().session {
SessionState::Loading => RouteDecision::Pending,
SessionState::Authenticated(_) => RouteDecision::Allow,
SessionState::SignedOut => RouteRedirect::replace("/sign-in")
.return_to(&view.env().current_route)
.into(),
SessionState::Failed(_) => RouteDecision::Deny,
};
let router: Widget = Router::<AppState>::new()
.with_path(view.state().current_path.clone())
.route_component(
"/account",
ProtectedRoute::new(account_access, AccountRoute),
)
.route_component("/sign-in", SignInRoute)
.into();
ProtectedRoute converts only its selected component. While the decision is
pending, denied, or redirecting, AccountRoute cannot register reducers,
resources, jobs, or local state. Use .pending(...) and .denied(...) to
replace the default progress and access-denied branches.
Authentication restoration remains an ordinary asynchronous Fission effect.
For example, a startup reducer can read a session from the Store and update
SessionState in its success or failure reducer. The route decision is a pure
derivation from that state; it is not itself a reducer.
Redirects replace history by default, preventing browser Back from immediately
retrying the rejected route. SSR returns an HTTP 302 without protected markup,
and denial returns HTTP 403. Static routes can emit a redirect document when
the decision is known at build time, but a static artifact cannot authenticate
a visitor. Route protection controls UI construction; APIs and server actions
must still enforce authorization independently.
Register the route reducer with .with_route_handler(...), then issue navigation from reducers with ctx.effects.navigate("/projects/42"), replace_route, navigation_back, navigation_forward, or navigation_go. A navigational Link::to(...) enters the same contract.
Field table
| | | |
|---|
| | The path string to match this frame. | Usually lives in GlobalState so reducers can update it explicitly. |
| | Ordered list of route patterns and lazily converted route components or page builders. | The router returns the first matching route. Order matters. |
| | Fallback builder when no route matches. | If omitted, the router renders a plain 404: {current_path} text node. |
Matching behavior
The current matcher is deliberately small:
/users/:id matches /users/123
/users/:id does not match /users/123/details
empty leading and trailing slashes are ignored during matching
parameter values are returned as strings in RouteParams This small contract makes routing easy to test. If a path renders the wrong screen, you only need to inspect the current path string and the ordered route list.
The active shell initializes Env.current_route and dispatches ShellRouteChanged before the first frame. Web additionally listens to popstate and hashchange. In WebRouteStrategy::Auto, an initial #/path keeps hash routing and an initial /path keeps pathname routing; applications can explicitly choose Path or Hash through WebApp::with_navigation(...).
The key point is that this contract does not require Router. A custom router can register the same route handler, read RouteLocation, issue the same navigation effects, and use SemanticsRegion::hyperlink(...) or Pressable::hyperlink(...) to produce genuine browser links.
Specific advice
Keep current_path normalized. Pick one format for trailing slashes, slug casing, and identifier encoding so reducers and tests do not have to guess what a valid path looks like.
Also keep route changes explicit. If a button means "go to /settings," dispatch a navigation action that updates state. Avoid scattering manual string mutations through unrelated reducers.
Production checklist
For Router, review the fields that change behavior before treating the widget as finished: current_path, routes, not_found. The goal is to make the product rule visible in state and actions, not hidden inside ad-hoc construction code.
If this widget appears inside an interactive flow, keep the surrounding action binding in the parent component and test that the flow still has one clear reducer path.
Check the semantics tree for the user-facing label or role that makes this widget understandable without relying only on pixels.
Add at least one component or harness test that confirms the visible text, semantic role, action dispatch, and layout constraint that matter for this widget in context.
If a screen starts repeating the same Router setup, extract a named component around this widget. That keeps the reference API small while making product code easier to read and safer for generated code to copy.