Routing
Introduction to Routing
Setting up the router, how an address turns into nested layouts and pages, the useRouter options, navigation modes, the not-found route and the error page.
MotifJS routing comes with the core. You define routes with a configuration array, place them on screen with RouterView, and navigate in code through Application.
The examples in this category come from the notes application in the starter template (MainLayout, NotesLayout, NotePage). Note, Folder and NotesService are the ones in the src/services/notes.ts file on the Components page.
How an address gets on screen
Routes are defined in a single RouteItem[] table. Routes nested with childs form a chain: the parent route is the layout, and the child's component sits in its RouterView. The same component can be given to several routes; the difference between the routes is carried by fields such as meta. An example table:
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}", control: NotePage },
],
});
export const routes: RouteItem[] = [
{
path: "/",
control: MainLayout,
childs: [
{ path: "/", redirect: "/notes" },
folder("notes"),
folder("pinned"),
folder("archive"),
{ path: "/settings", control: () => import("./pages/SettingsPage") },
],
},
];
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}", control: NotePage },
],
});
export const routes = [
{
path: "/",
control: MainLayout,
childs: [
{ path: "/", redirect: "/notes" },
folder("notes"),
folder("pinned"),
folder("archive"),
{ path: "/settings", control: () => import("./pages/SettingsPage") },
],
},
];
An address matches a chain in the table, and every link of the chain sits in the RouterView of the one before it. During navigation only the links that change are rebuilt: a layout that stays on the same route stays in place, while the component of another route is rebuilt even if it is the same class. In the example, the address /notes/3 matches the chain / (MainLayout) → /notes (NotesLayout) → /{id} (NotePage); going to /notes/5 rebuilds only NotePage, and going to /pinned rebuilds NotesLayout too. The address / goes to /notes through the redirect in the default child.
All the fields of the route table are on the Route Table page, and the placement of RouterView on the RouterView page.
Turning on the router
The route table is given with useRouter while the application is being built:
import { Application } from "@motifx/core";
import NotFoundPage from "./pages/NotFoundPage";
import { routes } from "./routes";
export function createApp(): Application {
const builder = Application.CreateBuilder();
const app = builder.build();
app.useDevelopment(import.meta.env.DEV);
app.useRouter({
routes,
mode: "history",
fallbacks: { notFound: NotFoundPage },
});
return app;
}
import { Application } from "@motifx/core";
import NotFoundPage from "./pages/NotFoundPage";
import { routes } from "./routes";
export function createApp() {
const builder = Application.CreateBuilder();
const app = builder.build();
app.useDevelopment(import.meta.env.DEV);
app.useRouter({
routes,
mode: "history",
fallbacks: { notFound: NotFoundPage },
});
return app;
}
If no root component is given to run, a RouterView is placed at the root and the first link of the chain lands there; in the example the application starts with createApp().run("#app"). When useDevelopment(true) is called before useRouter, the route table is checked in development mode; the situations the check warns about are described on the Route Table page.
| Option | What it does |
|---|---|
routes |
The route table. If no other options are needed, pass just the array: app.useRouter(routes). |
mode |
How the address is read and written; see the table below. |
fallbacks |
Components for an unmatched address (notFound) and for a page that cannot be built (error); see The not-found route and the error page. They accept every form the route's control field accepts, () => import(...) included. |
hooks |
onEntering, onEnter, onLeave, onUpdate: application-wide hooks for routes that have no hook of their own; see Route Hooks and Guards. |
scrollMemory |
Remembers the scroll position of every path; off by default. See Scroll memory. |
stack |
Mobile-style stack navigation; off by default. See Stack Navigation. |
The mode values:
| Mode | Behaviour |
|---|---|
'history' |
The path is the address bar's path and query (/notes?tag=work); navigation is written with history.pushState / replaceState. This is the default when mode is not given. |
'hash', 'file' |
Both work the same way: the path is the part after # (/index.html#/notes). When mode is not given and the page is opened from a file: address (as in Electron), this behaviour is chosen. 'history' mode cannot work under file:, because the address's path is a file path: even if mode: 'history' is given explicitly, the router works as in this row and development mode writes an MJX321 warning. |
'shell' |
The address bar and the browser history are never touched; the router always starts with /, and the page's address and query do not enter params. popstate/hashchange are not listened to: the browser's back/forward button or a popstate event does not move the router. stack has no effect in this mode. |
In 'history' mode the route table matches the whole path of the address; <base href> and Vite's base setting are not taken into account. If the application is published under a subpath (https://site.com/app/), the address becomes /app/notes, the /notes route does not match, and the page opens as not found. For such a deployment, write the routes together with the subpath (/app/notes), or use 'hash' mode; in 'hash' mode the path starts after #, so the address /app/#/notes matches the /notes route.
The not-found route and the error page
If no route matches, the fallbacks.notFound component is rendered into the outlet of the deepest layout that matches as a prefix of the URL (the shell does not disappear), the address bar is updated to the requested URL, and app.router.ok becomes false (app.router.params.path carries the requested path). If no fallback is defined, a built-in "404 - Not Found" panel is shown. fallbacks.error is used the same way when a route component cannot be loaded or throws (params.error); if it is not defined, a built-in error panel appears.
In the example, for /notes/3/x the deepest layout whose prefix matches is NotesLayout, and NotFoundPage is rendered into its output; for /missing, the only matching layout is MainLayout.
Router error codes
The codes the router produces: reports go to console.error and to the errorHandler listeners, warnings are written only in development mode, and thrown errors return to the calling line (see Error Handling). All the codes are on the Error and Warning Codes page:
| Code | When |
|---|---|
MJX301 |
A warning in development mode: the layout's RouterView was not built within 3000 ms, so the child route could not be attached. |
MJX302 |
href or navigateByName asked for a route whose name was not found; thrown. |
MJX303 |
A warning in development mode: a redirect loop or a redirect chain longer than 10 steps; the error page is shown at the requested address. |
MJX304 |
The route page could not be built (the constructor threw, the import failed, there is no default export, a ready instance that has been disposed); the error page is shown. The original cause is in the report's cause. |
MJX305 |
The error page could not be built either. |
MJX306 |
A route hook, onShow or a guard threw. With a route hook and onShow the navigation goes on; with a guard it is cancelled. |
MJX307 |
An error occurred while a route page was being disposed. |
MJX308 |
The transition animation of stack navigation failed. |
MJX309 |
Navigation was requested without calling useRouter; thrown. |
MJX317 |
Navigation fell over with an unexpected error; navigate returns { ok: false, cancelled: true, reason: 'error' }. |
The route table's check warnings in development mode (MJX310–MJX316, MJX319, MJX320) are on the Route Table page.
Next step
To learn how routes are defined, go on to The Route Table.