core concepts - greydragon888/real-router GitHub Wiki
This guide explains the mental model behind Real Router. Read it before diving into the API docs. Understanding these ideas will make everything else click.
| Term | Definition |
|---|---|
| Route | A named destination in your application, defined by a name and a URL path pattern. |
| Route Tree | The hierarchical structure of all registered routes, organized by dot-separated names. |
| Segment | A single node in the route tree. users.profile has two segments: users and users.profile. |
| Node | Synonym for segment. Used especially in the context of view subscriptions (useRouteNode). |
| State | An immutable snapshot of the current router location: name, params, and path. |
| Transition | The process of moving from one state to another, including guard execution and segment activation/deactivation. |
| Transition Node | The common ancestor segment where two routes diverge. Segments below it get deactivated and re-activated; segments above it stay untouched. |
| Guard | A function that can allow or block a transition. Returns true to allow, false (or any falsy value) to block. |
| Plugin | An observer that reacts to router lifecycle events (start, stop, transition success/error). Plugins cannot block transitions. |
| FSM | Finite State Machine. The router's internal lifecycle is modeled as an FSM with states: IDLE, STARTING, READY, TRANSITION_STARTED, LEAVE_APPROVED, DISPOSED. |
For the complete list of terms, see the Glossary.
Routes in Real Router use dot-separated names to express hierarchy. The name users.profile means "the profile route, nested under users." This isn't just a naming convention — it defines the tree structure the router uses to calculate transitions.
graph TD
root["(root)"]
home["home"]
users["users"]
users_list["users.list"]
users_profile["users.profile"]
admin["admin"]
admin_settings["admin.settings"]
root --> home
root --> users
root --> admin
users --> users_list
users --> users_profile
admin --> admin_settings
Each node in this tree corresponds to a segment. When you navigate to users.profile, the router activates both users and users.profile — the full path from root to destination.
You can define routes as a flat list using full dot-separated names, or as a nested tree using children. Both produce the same route tree:
// Flat list
const routes = [
{ name: "users", path: "/users" },
{ name: "users.list", path: "/list" },
{ name: "users.profile", path: "/:id" },
];
// Nested (children)
const routes = [
{
name: "users",
path: "/users",
children: [
{ name: "list", path: "/list" },
{ name: "profile", path: "/:id" },
],
},
];Use children when you want to co-locate related routes. Use the flat list when routes are spread across modules or when you're building routes dynamically.
A State object is an immutable snapshot of where the router is right now. It's frozen with Object.freeze() — you can read it, but never mutate it.
const state = router.getState();
state.name; // "users.profile" — the active route name
state.params; // { id: "42" } — URL and query parameters
state.path; // "/users/42" — the resolved URL pathEvery successful navigation produces a new State object. The previous one is preserved and accessible via router.getPreviousState().
For the full State type and all fields, see State.
When you navigate from one route to another, the router calculates which segments need to change. It doesn't tear down and rebuild everything — only the parts that actually differ.
Given a transition from admin.users to home:
- The router finds the intersection — the deepest common ancestor of both routes.
- Everything below the intersection on the current side gets deactivated (deepest first).
- Everything below the intersection on the target side gets activated (shallowest first).
graph LR
subgraph "Deactivated (leaving)"
A2["admin"]
A3["admin.users"]
end
subgraph "Transition Node"
T["(root) — intersection"]
end
subgraph "Activated (entering)"
B1["home"]
end
A3 --> A2 --> T --> B1
For a transition from users.list to users.profile, the intersection is users. Only users.list deactivates and users.profile activates — the users segment stays mounted.
graph LR
subgraph "Deactivated"
D["users.list"]
end
subgraph "Transition Node"
T["users — intersection"]
end
subgraph "Activated"
A["users.profile"]
end
D --> T --> A
This is exactly why useRouteNode exists. A component subscribed to "users" won't re-render when navigating between users.list and users.profile — because users is the intersection, not a changed segment. Only components subscribed to nodes that appear in toActivate or toDeactivate will re-render.
This makes Real Router efficient by default. Deep component trees don't re-render on every navigation — only the parts that actually changed.
The router's lifecycle is a finite state machine. At any moment, the router is in exactly one of these states:
stateDiagram-v2
[*] --> IDLE
IDLE --> STARTING : start()
STARTING --> READY : initialization complete
STARTING --> IDLE : initialization failed
READY --> TRANSITION_STARTED : navigate()
READY --> IDLE : stop() / dispose()
TRANSITION_STARTED --> LEAVE_APPROVED : canDeactivate guards pass
LEAVE_APPROVED --> READY : canActivate guards pass / complete
LEAVE_APPROVED --> READY : cancel / fail
TRANSITION_STARTED --> READY : cancel / fail (before deactivate guards)
TRANSITION_STARTED --> TRANSITION_STARTED : new navigate() (cancels previous)
TRANSITION_STARTED --> IDLE : stop() / dispose()
LEAVE_APPROVED --> IDLE : stop() / dispose()
IDLE --> DISPOSED : dispose()
DISPOSED --> [*]
IDLE — The router exists but hasn't started. Calling navigate() will fail with ROUTER_NOT_STARTED. This is the initial state and also where the router returns after stop().
STARTING — start() was called and the router is performing its initial navigation. Guards run, plugins initialize. If initialization fails, the router returns to IDLE.
READY — The router is active and ready for navigation. This is the normal operating state. router.isActive() returns true here.
TRANSITION_STARTED — A navigation is in progress. canDeactivate guards are running. Starting a new navigation here cancels the current one.
LEAVE_APPROVED — An intermediate phase entered after all canDeactivate guards pass and before canActivate guards run. The current route hasn't changed yet, but departure is confirmed. subscribeLeave() callbacks and the onTransitionLeaveApprove plugin hook fire here. The router returns to READY when the transition completes, fails, or is cancelled.
DISPOSED — The router has been permanently terminated via dispose(). No further navigation is possible. All mutating methods throw an error. This state is terminal — there's no way back.
For starting and stopping the router, see start and stop.
Real Router is built around a few ideas that shape how everything works.
Navigation isn't a side effect — it's a state transition. When you call navigate(), the router moves through a well-defined sequence: deactivate guards run, the LEAVE_APPROVED phase fires departure hooks, activate guards run, segments activate/deactivate, state updates. The result is always a new immutable State object or a rejected Promise with a clear error code.
This makes the router predictable and testable. You can reason about what will happen before it happens.
Real Router doesn't render anything. It doesn't know about React, Vue, or any other framework. It produces state, and your view layer subscribes to that state.
This is intentional. The router's job is to manage location state. Your view's job is to render based on that state. Keeping these concerns separate means you can use the router anywhere — browser, server, mobile, tests — without dragging in view-layer dependencies.
Subscribe to state changes with subscribe, or use useRouteNode in React for optimized per-segment subscriptions.
The core router has no concept of a browser URL. It works with route names and params. URL parsing and history management are handled by the browser plugin, which is just a plugin like any other.
This means the router works identically in server-side rendering, tests, and non-browser environments. The browser plugin adds URL behavior on top — it doesn't bake it in.
Guards (canActivate, canDeactivate) run before state changes and can block navigation. Plugins run after state changes and can only observe. This separation is deliberate: blocking logic belongs in guards, side effects belong in plugins.
See addActivateGuard and addDeactivateGuard for guard registration. See usePlugin for plugin registration.
Route paths use a pattern syntax built into Real Router's internal path-matcher engine. The key patterns:
| Pattern | Example | Matches |
|---|---|---|
:param |
/users/:id |
Required URL segment |
:param? |
/users/:id? |
Optional URL segment |
*splat |
/files/*path |
Catch-all (any remaining path) |
?query |
/users?page&sort |
Declared query parameters |
~ prefix |
~/absolute |
Absolute path (ignores parent) |
<constraint> |
/:id<\\d+> |
Regex constraint on a parameter |
Child route paths are relative to their parent by default. A child with path /list under a parent with path /users matches /users/list.
For the full path syntax reference and examples, see Route.
Beyond name and path, routes accept several configuration options:
forwardTo — Redirects navigation to another route. Useful for URL aliases and default child routes. Guards on the source route are skipped; only guards on the destination run. Accepts a static string or a callback (getDependency, params) => string for dynamic redirects.
// Static alias
{ name: "users", path: "/users", forwardTo: "users.list" }
// Dynamic — redirect based on runtime state
{ name: "dashboard", path: "/dashboard", forwardTo: (getDependency) => {
const role = getDependency("userRole");
return role === "admin" ? "admin.dashboard" : "user.dashboard";
}}defaultParams — Values merged into state.params when this route is active and the URL doesn't provide those params. Useful for pagination defaults, filter presets, and similar cases.
encodeParams / decodeParams — Custom serialization for route parameters. Use encodeParams to convert state params to URL-safe strings, and decodeParams to parse them back. Useful when params contain complex types (dates, arrays, objects) that need custom URL encoding.
canActivate / canDeactivate — Guards defined inline on the route config. These are factory functions: (router, getDependency) => (toState, fromState, signal?) => boolean | Promise<boolean>. See addActivateGuard for the full guard API.
For the complete Route interface reference, see Route.
When creating the router with createRouter, you pass options that control global behavior:
defaultRoute — The route to navigate to when start() is called without a matching path, or when navigateToDefault() is called. Accepts a string or a callback for dynamic resolution.
allowNotFound — When true (the default), navigating to an unknown route succeeds and produces a state with the unmatched name. When false, it rejects with ROUTE_NOT_FOUND. The special UNKNOWN_ROUTE state ("@@router/UNKNOWN_ROUTE") is outside the route tree — it's set programmatically via navigateToNotFound() or automatically during start() and popstate events. See navigateToNotFound.
trailingSlash — Controls how trailing slashes are handled: "strict" (exact match), "never" (always strip), "always" (always add), or "preserve" (keep as-is, the default).
queryParamsMode — Controls how query parameters outside declared ?param patterns are handled. "loose" (default) passes them through; "strict" ignores undeclared params; "default" uses route-node defaults.
For all options and their defaults, see RouterOptions.