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:

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

alias

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.