Routing
The Route Table
RouteItem fields, nested routes, redirect and alias, path parameters, querying the route table, lazy routes and the route check in development mode.
Defining a route (RouteItem)
Routes are a RouteItem[] array. Each item maps a path (path) to a component (control). The example table shows the commonly used fields of RouteItem together: a pattern-constrained, named route ({id(\d+)}, name), a layout alias, an optional parameter with a default, and a redirect (an extended version of the table on the Introduction to Routing page):
import type { RouteItem } from "@motifx/core";
import MainLayout from "./layouts/MainLayout";
import NotesLayout from "./layouts/NotesLayout";
import NoteEmptyPage from "./pages/NoteEmptyPage";
import NotePage from "./pages/NotePage";
import type { Folder } from "./services/notes";
const folder = (name: Folder): RouteItem => ({
path: `/${name}`,
control: NotesLayout,
meta: { folder: name },
childs: [
{ path: "/", control: NoteEmptyPage },
{ path: "/{id(\\d+)}", name: `${name}-note`, control: NotePage },
],
});
export const routes: RouteItem[] = [
{
path: "/",
control: MainLayout,
childs: [
{ path: "/", redirect: "/notes" },
{ ...folder("notes"), alias: "/my-notes" },
folder("pinned"),
folder("archive"),
{ path: "/settings/{section?:appearance}", name: "settings", control: () => import("./pages/SettingsPage") },
{ path: "/n/{id}", redirect: "/notes/{id}" },
],
},
];
import MainLayout from "./layouts/MainLayout";
import NotesLayout from "./layouts/NotesLayout";
import NoteEmptyPage from "./pages/NoteEmptyPage";
import NotePage from "./pages/NotePage";
const folder = (name) => ({
path: `/${name}`,
control: NotesLayout,
meta: { folder: name },
childs: [
{ path: "/", control: NoteEmptyPage },
{ path: "/{id(\\d+)}", name: `${name}-note`, control: NotePage },
],
});
export const routes = [
{
path: "/",
control: MainLayout,
childs: [
{ path: "/", redirect: "/notes" },
{ ...folder("notes"), alias: "/my-notes" },
folder("pinned"),
folder("archive"),
{ path: "/settings/{section?:appearance}", name: "settings", control: () => import("./pages/SettingsPage") },
{ path: "/n/{id}", redirect: "/notes/{id}" },
],
},
];
What these fields do:
- A segment that does not fit the pattern does not match the route:
/notes/3matches,/notes/abcdoes not, andfallbacks.notFoundis shown. - The path of a route given a
nameis produced withhref/navigateByName:app.router.href("archive-note", { id: 7 })→/archive/7. - An alias opens the same route at a second address, and the alias stays in the address bar:
/my-notes/3. - An optional parameter that is missing from the address gets its default: at
/settings,params.sectionis"appearance", and at/settings/notesit is"notes". {id}in aredirectstring is filled with the matched parameter:/n/3→/notes/3.
RouteItem fields
| Field | Description |
|---|---|
path |
The route path. Parameters are in the {name} form. '/' for a layout and the default child; every child path starts with / (otherwise development mode warns with MJX311). At the same depth a static segment comes before a parameter (/archive beats /{slug}). |
control |
The route component. It can be a component class, a ready component instance, () => Component, an Options API factory, () => import(...) (lazy) or a Promise. It is not required only on a route that defines redirect. A route given a ready instance (or a Promise that resolves to one) behaves like keepAlive: the instance is built once, detached from the outlet on leaving, and the same instance is attached again on return; it is not rebuilt on a parameter change (onUpdate runs). app.router.evict() does not dispose this instance, app.dispose() does. If the route is entered while the instance has been disposed, the page cannot be built: MJX304 is reported (with MJX318 as the cause) and the error route is shown; a page that wants fresh state on every entry gives a class or a factory. |
childs |
Child routes. If defined, this route is a layout route. |
name |
For reaching the route by name (optional); href and navigateByName use this name. |
meta |
Free-form data that guards and hooks can use ({ requiresAuth: true, title: '...' }). The meta objects in the matched chain (layout → child) are merged; a requiresAuth written on a layout shows up in its children as well. In the example, meta.folder is written on the layout route and read on the note route under it too. |
extend |
Extra data attached to the route; merged along the chain like meta. extend.targetOutlet picks the named RouterView the route's component will sit in (see RouterView). |
keepAlive |
If true, the component instance is kept: on leaving it is detached from the outlet and cached (onDeactivated), and on return the same instance is attached again in the same position (onActivated); both are passed on to the page's visible child components. The cache is emptied with app.router.evict(name \| route) (with no argument: all of them; the instance on screen at that moment is skipped) or app.dispose(). |
redirect |
Redirects to another path when this route is reached: a path string with {param} placeholders, or a (to) => path function. Details: Redirect and alias. |
alias |
Extra path(s) that open the same route; the alias stays in the address bar. Details: Redirect and alias. |
validate(e) |
Called during matching with { uri, key, routes, params }; if it returns false, this route does not count as matched (another route or fallbacks.notFound takes over). The validate of every route in the chain runs. key is the route's name, or its full path if it has none (its own path for an alias match too); routes is the route table keyed by the same rule (a Map): routes.get(e.key) is the route being validated, and another route is reached by its name or full path. |
onShow(component) |
Called with the component instance every time the route's component is placed in its own RouterView: the first show, rebuilding on a parameter change, coming back from the keepAlive cache, and being placed again when the RouterView is rebuilt. It is called for layout routes too; it is not called again for a layout that stays in place throughout a navigation. An error thrown by the hook is reported with the code MJX306 and does not stop the navigation. |
The route hooks tied to navigation (onEntering, onEnter, onLeave, onUpdate) are described on the Route Hooks and Guards page.
Redirect and alias
/n/{id} in the example is a redirect in string form. If the target depends on a calculation, give a function:
const shortcuts: RouteItem[] = [
{ path: "/n/{id}", redirect: "/notes/{id}" },
{ path: "/find/{q}", redirect: (to) => "/notes?q=" + encodeURIComponent(to.params.q) },
];
const shortcuts = [
{ path: "/n/{id}", redirect: "/notes/{id}" },
{ path: "/find/{q}", redirect: (to) => "/notes?q=" + encodeURIComponent(to.params.q) },
];
redirect
- In the string form, the
{param}placeholders are filled with the matched parameters. The query string (?…) and#…of the source address are carried over to the target unless the target contains its own query/#:/n/3?from=mail→/notes/3?from=mail. - The function form receives
{ path, params, meta }; the path it returns is used as it is. - The redirect is applied right after matching, before
onLeaveand theuseGuardguards; guards and hooks run only for the target route. The source route's component is never created. - The chain is followed (
/a→/b→/c). A redirect that goes over 10 steps or comes back to a path passed earlier is stopped, thefallbacks.errorpage is shown at the requested address, and in development mode anMJX303warning is written to the console. - Only the target enters the browser history. If the address bar already shows the source path (first load, back/forward), the entry is changed with
replace, so the "back" button does not get stuck on the redirect. - Where the target came from is read through
to.redirectedFrom(in the guard andonLeavecontext) and theredirectedFromfield of theonRouterChangedpayload. - Only the
redirectof the matched leaf route is applied. If a layout has a'/'default child, the leaf that matches at the layout's path is that child; in that case aredirectwritten on the layout does not run, so write the redirect on the default child. The root route in the example does this: at the address/, the one that redirects is the'/'child ofMainLayout.
alias
- An alias is added to the parent path by the same rule as
path(alias: '/detail/{id}'under/notes→/notes/detail/{id}). Parameters are read from the alias pattern. - The children open under a layout's alias too: in the example,
/my-notes/5→NotePage. - The alias and the main path are the same route: moving between them with the same parameters keeps the component; if the parameters change,
onUpdateruns and the page is rebuilt, as on any parameter change (keepAlive: truekeeps it). ThekeepAlivecache is shared as well. - If an alias clashes with the main path of another route, the main path wins. The check in development mode warns when an alias equals its own path (
MJX315), when two routes use the same alias (MJX316), and when a child alias is missing the/prefix (MJX314). app.router.fullPathgives the matched pattern (the alias pattern for an alias), andapp.router.aliasOfgives the main pattern (nullif it is not an alias): at/my-notes/5,/my-notes/{id(\d+)}and/notes/{id(\d+)}.navigateByNameandhrefalways produce the main path.RouterLinkclasses look at the URL: while at/my-notes, a link withto="/notes"does not count as active.
Path parameters
Parameters are defined with the {name} syntax:
| Syntax | Meaning |
|---|---|
{id} |
A required parameter. |
{id:1} |
Required; 1 is used only when href / navigateByName produces a path and no value is given (the pattern /h/{id:1} does not match the address /h). |
{id?} |
Optional. |
{id?:5} |
Optional, with the default 5: if it is missing from the address, params.id becomes '5'. |
{id(\d+)} |
A pattern constraint: the segment matches only if it fits the regular expression in the parentheses (case-insensitive; capturing groups in the pattern are ignored; ? and : are part of the pattern). It combines with ? and a default: {id(\d+)?:1}. The pattern applies to a single segment and cannot contain /. In a JavaScript string the backslash is written twice: "/{id(\\d+)}". |
{path+} |
One or more segments: for /files/a/b/c, params.path becomes 'a/b/c' (plain text; each segment is decoded separately, and you can split('/') the parts if needed). For href, give 'a/b' or ['a','b'], and the segments are encoded separately. It combines with a pattern, and the pattern applies to every segment: {ids(\d+)+}. |
{path*} |
Like {path+}, but there may be no segment at all (optional like {path?}, and it takes a :default). |
A : outside the curly braces and the rest of the segment drop out of the pattern (/user/:id → /user/, which matches no address); in development mode the check warns with MJX319.
In the ordering, a route containing {…+} or {…*} comes after routes at the same depth with the same number of static segments: /f/{a}/{b} before /f/{rest+}; /docs/{rest+}, with its static segment, before /{a}/{b}. For an unusable pattern (one that does not compile, has an unclosed ( or contains /), the check warns in development mode with MJX320.
href, navigateByName and the redirect string fill the {…} placeholders, and the values are encoded with encodeURIComponent (href('tag', { name: 'a/b' }) → /tags/a%2Fb, { name: 'café' } → /tags/caf%C3%A9).
Parameters are reached from the navigation context or the router:
import { useNavigation } from "@motifx/core";
const nav = useNavigation();
const noteId = Number(nav.params.id);
import { useNavigation } from "@motifx/core";
const nav = useNavigation();
const noteId = Number(nav.params.id);
params also contains the query string values. Like path parameters, query values are strings and arrive with their percent-encoding decoded (/notes/5?tab=history → { id: '5', tab: 'history' }, /tags/caf%C3%A9?q=a%20b → { name: 'café', q: 'a b' }); in the path + is a plain character, while in the query it is a space; values such as true, null, 2025-10-27 or {"a":1} stay text as well. If you need a number, a boolean or a date, convert the value yourself (Number(params.id)). A repeated key (?tag=work&tag=home) becomes an array of strings (['work', 'home']). A path parameter with the same name overrides the query value. The #… part never enters params.
router.uri, the uri of the onRouterChanged event, the middleware's ctx.uri and the guard's to.path are in address form: percent-encoded, the same as the address bar (/tags/caf%C3%A9). To display one, use decodeURIComponent; for the value, use params. navigate, the to of RouterLink and the onActive/onExact path can be written raw (/tags/café) or encoded; both go to the same address and make the same link active.
The query is read only from the address being navigated to: in history mode, if there is no ? in the address, params contains no query values (the previous page's query is not carried over). In hash/file mode, the query before # (/?x=1#/a) is page-level; it reaches params on every route that has no query of its own, and if the route has its own query (#/b?z=3), that one is used instead.
Querying the route table — resolve, href, routes
app.router carries three members for querying the route table without navigating:
// Which route does an address match? (does not navigate, does not touch the address bar)
const { chain, result } = app.router.resolve("/notes/5");
if (result.ok) console.log(result.route?.name, result.params.id, result.meta.folder); // "notes-note" "5" "notes"
chain; // ['/', '/notes', '/5']
result.chain.map((r) => r.path); // ['/', '/notes', '/{id(\\d+)}']: definitions from the root to the route
// Every route in definition order (aliases excluded); for building a menu or a site map
const paths = app.router.routes.map((r) => r.fullPath);
// Which route does an address match? (does not navigate, does not touch the address bar)
const { chain, result } = app.router.resolve("/notes/5");
if (result.ok) console.log(result.route?.name, result.params.id, result.meta.folder); // "notes-note" "5" "notes"
chain; // ['/', '/notes', '/5']
result.chain.map((r) => r.path); // ['/', '/notes', '/{id(\\d+)}']: definitions from the root to the route
// Every route in definition order (aliases excluded); for building a menu or a site map
const paths = app.router.routes.map((r) => r.fullPath);
The path of a named route is produced with href; for the to of a RouterLink, for menus and breadcrumbs. In the example the link's target is produced from a named route (nav is the router returned by useNavigation()):
<RouterLink to={nav.href(`${folder}-note`, { id: note.id })}>{note.title}</RouterLink>
const link = motifComponent(RouterLink, {
to: nav.href(`${folder}-note`, { id: note.id }),
childs: [motifComponent("text", (node: ComponentBase) => node.bindings.add("textContent", note, "title"))],
});
<RouterLink to={nav.href(`${folder}-note`, { id: note.id })}>{note.title}</RouterLink>
const link = motifComponent(RouterLink, {
to: nav.href(`${folder}-note`, { id: note.id }),
childs: [motifComponent("text", (node) => node.bindings.add("textContent", note, "title"))],
});
| Member | Returns |
|---|---|
resolve(uri) |
RouteResolution: { chain, result }. chain has the same form as app.router.chain (string[], each route's own path with its parameters filled in). result is the ResolveResult that navigate would return when going to that address: { ok, uri, fullPath, route, chain, params, meta, extend, aliasOf }; result.chain is the route definitions from the root to the route (RouteItem[]). If there is no match, result.ok is false and chain is []. Guards, middleware and redirects do not run. |
href(name, params?) |
The path of the named route with its parameters filled in (the same calculation as navigateByName). 0 and false values are written; for a parameter given as undefined, null or '', or not given at all, the pattern's default is used (if there is no default, that segment is not written). Parameters that are not in the pattern are not written; if you need a query, append it to the produced path (href("notes-note", { id: 3 }) + "?tab=history"). If the name does not exist, it throws MJX302. |
routes |
RouteInfo[]: { fullPath, name, meta, route, chain }. A layout and its default child are separate items; chain is the route definitions from the root to this route. |
Lazy loading
Using () => import(...) in the control field loads the route component only when it is needed and gives automatic code splitting. A page given this way is split into a separate chunk and downloaded the first time the route is visited:
const settings: RouteItem = { path: "/settings/{section?:appearance}", name: "settings", control: () => import("./pages/SettingsPage") };
const settings = { path: "/settings/{section?:appearance}", name: "settings", control: () => import("./pages/SettingsPage") };
The loaded module must export the page as default. For a named export, map it: control: () => import("./pages/SettingsPage").then((m) => m.SettingsPage). The page cannot be built with a module that has no default export, or with some other object that is not a component: MJX304 is reported (with MJX127 as the cause) and fallbacks.error is shown.
fallbacks.notFound and fallbacks.error support the same lazy form.
A route's import() does not use a loading view or retries. To lazily load a heavy part inside a page, show a view while it loads and retry on a mobile network, use the Lazy component (see Retrying).
Route check in development mode
When app.useDevelopment(true) is called before useRouter, the route table is checked while it is being set up; a problematic definition is written to the console as a warning, and the router keeps working:
| Code | When |
|---|---|
MJX310 |
The route path is empty (''); for the default child, write '/'. |
MJX311 |
A child route's path does not start with / ('notes'). |
MJX312 |
A child route ends up at the same full path as its parent (except the default child '/'). |
MJX313 |
Several routes end up at the same full path. |
MJX314 |
A child route's alias does not start with /. |
MJX315 |
An alias is equal to the route's own path. |
MJX316 |
Several routes use the same alias. |
MJX319 |
There is a : outside the curly braces in a path or an alias (/user/:id). |
MJX320 |
A parameter pattern cannot be used: it does not compile, has an unclosed ( or contains /. |
Next step
To learn where route components are placed on screen, go on to RouterView.