The @lwrjs/router
package provides modules for client-side routing (lwr/router
) and navigation (lwr/navigation
), which export APIs to create a router, navigate, generate URLs and subscribe to navigation events. Client-side routing enables the creation of a Single Page Application (SPA).
LWR routers can be customized with configuration and hooks. They can also be nested, to create a hierarchy in an application.
See the RFC on LWR routing APIs here.
A router is a piece of code that manages client-side navigation changes. All navigation events flow through a router for processing. Use the createRouter(config: RouterConfig)
API to initialize a LWR router:
import { createRouter } from 'lwr/router';
createRouter({
routes: [
/* see Route Definitions section */
],
basePath: '/my-site',
i18n: {
locale: 'es',
defaultLocale: 'en-US'
}
caseSensitive: true,
});
It takes a configuration object as an argument:
type RouterConfig = {
routes?: RouteDefinition[]; // see Route Definitions section, default = []
basePath?: string; // a path prefix applied to all URIs, default = ''
i18n?: I18NRouterConfig; // a i18n config that may effect the path prefix applied to all URIs, default = {locale: 'en-US', defaultLocale: 'en-US'}
caseSensitive?: boolean; // true if URIs should be processed case sensitively, default = false
};
The router processes incoming navigation events (i.e. location changes), which enter the router as page references. A page reference is a location in JSON form, passed to the router via the navigate()
API
interface PageReference {
type: string;
attributes: { [key: string]: string | null };
state: { [key: string]: string | null };
}
The router uses its route definitions to determine if a location is valid. If so, the navigation event is accepted and a user will see updated content in their browser.
The most important part of the RouterConfig
is the array of route definitions. The router uses these to verify and process incoming location changes. A location is only valid if it can be matched to a RouteDefinition
. The application will fail to navigate given an invalid location. Each RouteDefinition
has this shape:
interface RouteDefinition<TMetadata = Record<string, any>> {
id: string;
uri: string;
page: Partial<PageReference>;
handler: () => Promise<{ default: RouteHandler }>;
patterns?: { [paramName: string]: string };
exact?: boolean;
metadata?: TMetadata;
}
containing the following properties:
-
id
: eachRouteDefinition
must have a unique identifier -
uri
: a string pattern for URIs which match thisRouteDefinition
; the grammar is fully defined in an RFC and includes these characters:-
/
: path separator -
:parameter
: captures a variable from a path or query parameter; must be alpha-numeric (i.e. [a-zA-Z0-9]) -
?
: denotes the beginning of the query string -
&
: query parameter separator
-
-
page
: shape for page references which match thisRouteDefinition
; the usage is detailed in an RFC and allows:-
:parameter
bindings: map a path or query parameter from theuri
to anattributes
orstate
property - literal bindings: hard-code the
type
, anattribute
, orstate
property to a literal value
-
-
handler
: aPromise
to a module which is called when aRouteDefinition
is matched by a location; see the Route Handlers section -
patterns
(optional): a regular expression which a parameter must match in order to be valid; described in an RFC here -
exact
(optional, default =true
): see the Nesting Router Containers section -
metadata
(optional): developer-defined metadata attached toRouteDefinition
; see the RFC here and a recipe here
Important: The
routes
array seen in LWR app configuration is for server-side routes (see RFC here), and is unrelated to the@lwrjs/router
package.
Here is an example RouteDefinition
for a recipe:
// RouteDefinition for a page in a recipe website
{
id: 'recipe',
uri: '/recipes/:category/:recipeId?units=:units&yummy=yes',
patterns: {
recipeId: '[0-9]{3}', // "recipeId" must be a 3 digit number
},
page: {
type: 'recipe_page', // matching page references must be of type "recipe_page"
attributes: {
recipeId: ':recipeId', // straightforward attribute binding
cat: ':category', // bind the "category" path parameter to the "cat" attribute
units: ':units', // bind the "units" query parameter to an attribute
},
state: {
code: 'abc123', // hard-coded state literal
},
},
handler: () => import('my/recipeHandler'),
}
This URI and page reference match the recipe RouteDefinition
:
// URI -> https://www.somewhere.com/recipes/desserts/010?units=metric&yummy=yes&extra=foo (extra query params are allowed)
// page reference:
{
"type": "recipe_page", // type matches
"attributes": {
// all bound attributes have values
"recipeId": "010", // the "recipeId" matches the pattern
"cat": "desserts",
"units": "metric"
},
"state": {
"code": "abc123", // the state literal matches
"extra": "foo" // extra state properties are allowed
}
}
This URI and page reference do not match:
// URI -> https://www.somewhere.com/r/desserts/abc (parameters and literals are missing or malformed)
// page reference:
{
"type": "awful_page", // type DOES NOT match
"attributes": {
"recipeId": "lol", // the "recipeId" DOES NOT match the pattern
"extra": "bad" // extra attributes ARE NOT allowed
// the "cat" and "units" attributes are missing
},
"state": {
"code": "fail" // the state literal IS NOT equal
}
}
See more
RouteDefinition
examples with matching and non-matching page references in the RFC.
When the router matches an incoming location to a RouteDefinition
, it accesses its RouteDefinition.handler
to determine the associated "view". A view is the component to display when the application navigates to a location.
// types related to the RouteHandler
interface RouteDefinition {
handler: () => Promise<{ default: RouteHandler }>; // `export default` a RouteHandler module
// other properties...
}
interface RouteHandler {
new (callback: (routeDestination: RouteDestination) => void): void; // denotes a Class
dispose(): void;
update(routeInfo: RouteInstance): void;
}
interface RouteDestination {
// provided by `RouteHandler.update()` via a callback
viewset: ViewSet;
}
interface ViewSet {
[namedView: string]: (() => Promise<Module>) | ViewInfo;
}
interface ViewInfo {
module: () => Promise<Module>;
specifier: string;
}
interface RouteInstance {
// location information passed to `RouteHandler.update()`
id: string; // RouteDefinition.id
attributes: { [key: string]: string | null };
state: { [key: string]: string | null };
pageReference: PageReference;
}
Note: Modules are always provided via promises. This allows the module code to be lazily loaded, which improves performance of the application.
Given information on the current location (i.e. a RouteInstance
), the job of the RouteHandler
module is to provide a set of views via a callback from its update()
function:
// "my/recipeHandler" RouteHandler module
import type { Module, RouteHandlerCallback } from 'lwr/router';
export default class RecipeHandler {
callback: RouteHandlerCallback;
constructor(callback: RouteHandlerCallback) {
this.callback = callback; // Important: maintain a reference to the callback
}
dispose(): void {
// perform cleanup tasks
}
update(routeInfo: RouteInstance): void {
// called every time a RouteDefinition with this handler matches a location during processing
const {
attributes: { cat }, // location information
} = routeInfo;
const category = cat || 'entree'; // cat may be null
const viewSpecifier = `my/${category}Recipe`; // e.g. "my/dessertRecipe"
this.callback({
viewset: {
// return view component info based on the recipe's category
default: {
module: (): Promise<Module> => import(viewSpecifier),
specifier: viewSpecifier,
},
},
});
}
}
See the
RouteHandler
RFC here and some example handlers here.
The Router Module Provider can generate a router based on a static JSON file. A generated router consumes its configuration from a portable JSON file rather than a JavaScript module. Static configuration can be easier to author and to maintain. This approach is most helpful for straightforward use cases.
The Router Module Provider is not a default module provider, so it must be added to the project configuration. Learn more in Configure a LWR Project.
Add "@lwrjs/router/module-provider" as a dependency in
package.json`.
// package.json
{
"dependencies": {
"@lwrjs/router/module-provider": "0.7.1"
}
}
Register the Router Module Provider in lwr.config.json
.
// lwr.config.json with the Router Module Provider and the LWR default module providers
{
"moduleProviders": [
"@lwrjs/router/module-provider",
"@lwrjs/app-service/moduleProvider",
"@lwrjs/lwc-module-provider",
"@lwrjs/npm-module-provider"
]
}
When registering the module provider, optionally configure the directory location of the router JSON files.
// lwr.config.json
{
"moduleProviders": [
["@lwrjs/router/module-provider", { "routesDir": "$rootDir/config/router" }],
"@lwrjs/app-service/moduleProvider",
"@lwrjs/lwc-module-provider",
"@lwrjs/npm-module-provider"
]
}
If a configuration is not specified when registering the Router Module Provider, it uses this default configuration.
{
"routesDir": "$rootDir/src/routes"
}
To automatically register client-side routes with the server, specify them as "sub routes" by pointing to their Route JSON file.
// lwr.config.json
{
"routes": [
{
"id": "spa",
"path": "/site",
"rootComponent": "my/spa",
"subRoutes": "$rootDir/src/routes/client.json"
}
]
}
If the client-side routes contain these uri
s:
- "/"
- "/about"
- "/:id"
Then the LWR server will automatically register these
path
s: - "/site"
- "/site/about"
- "/site/:id"
This allows users to do a full page refresh on a client-side route without getting a 404.
The Router Module Provider generates a router module based on JSON configuration: LwrRouterConfig
.
interface LwrRouterConfig {
basePath?: string;
caseSensitive?: boolean;
routes: LwrConfigRouteDefinition[];
}
interface LwrConfigRouteDefinition<TMetadata = Record<string, any>> {
// These properties are the same as in RouteDefinition
id: string;
uri: string;
page?: Partial<PageReference>;
patterns?: { [paramName: string]: string };
exact?: boolean;
metadata?: TMetadata;
// These properties are different than RouteDefinition
// A Route Definition must have 1 or the other, but not both
handler?: string; // a STRING reference to the handler class
component?: string; // a STRING reference to a page component
}
The LwrRouterConfig
contains the same properties which are passed to createRouter()
. The LwrConfigRouteDefinition
contains the same properties as RouteDefinition
, except for:
-
handler
: A string reference to theRouteHandler
class specifier, rather than a function. -
component
: A string reference to the view component specifier. This is a shortcut so the view component can be specified directly, without authoring aRouteHandler
. ALwrConfigRouteDefinition
must contain ahandler
or acomponent
, but not both.
Note:
LwrConfigRouteDefinition
is pure JSON, which is why it cannot contain any functions likeRouteDefinition
does.
Here is a Router config example.
// src/routes/website.json
{
"routes": [
{
"id": "home",
"uri": "/",
"component": "examples/home",
"page": {
"type": "home"
},
"metadata": {
"title": "Home"
}
},
{
"id": "namedPage",
"uri": "/:pageName",
"handler": "examples/namedPageHandler",
"page": {
"type": "namedPage",
"attributes": {
"pageName": ":pageName"
}
}
}
]
}
Import and use a router generated using the JSON above.
// src/modules/my/app/app.js
import { LightningElement } from 'lwc';
import { createRouter } from '@lwrjs/router/website'; // "website" refers to src/routes/website.json
export default class MyApp extends LightningElement {
router = createRouter();
}
<!-- src/modules/my/app/app.html -->
<template>
<lwr-router-container router="{router}">
<lwr-outlet></lwr-outlet>
</lwr-router-container>
</template>
The generated router module specifier is: @lwrjs/router/<name of the JSON config file>
. It provides a createRouter()
function that is identical to the static createRouter()
function, except that it does not take a routes
array, since the routes are configured in the JSON file instead. If basePath
or caseSensitive
is specified in both the JSON file and the createRouter()
call, then the latter takes precedence.
See this documentation to learn about routing during server-side rendering.
In order to use a router in an application, it must be attached to the DOM. This is done with a router container, provided by the lwr-router-container
component.
A router container provides "navigation context", meaning that it is responsible for processing all navigation wires and events from its descendants in the DOM (e.g. my-nav
and lwr-outlet
in the example code below).
<!-- my/app/app.html -->
<template>
<lwr-router-container
router="{router}"
onhandlenavigation="{handleNavigation}"
onprenavigate="{preNavigate}"
onpostnavigate="{postNavigate}"
onerrornavigate="{errorNavigate}"
>
<my-nav></my-nav>
<lwr-outlet><!-- See the Outlet section below --></lwr-outlet>
</lwr-router-container>
</template>
// my/app/app.ts
import { LightningElement } from 'lwc';
import { createRouter } from 'lwr/router';
import { ROUTE_DEFINITIONS } from './routeDefinitions';
export default class MyApp extends LightningElement {
router = createRouter({ routes: ROUTE_DEFINITIONS });
approvedCategories = ['apps', 'entrees', 'sides', 'desserts'];
handleNavigation(e: CustomEvent): void {
console.log('navigate() called with page reference:', e.detail);
}
preNavigate(e: CustomEvent): void {
const {
next: {
route: { pageReference },
},
} = e.detail;
const {
attributes: { cat },
} = pageReference;
console.log('navigation event incoming with page reference:', pageReference);
if (!this.approvedCategories.includes(cat)) {
// REJECT unapproved recipe categories
e.preventDefault();
}
}
postNavigate(e: CustomEvent): void {
const {
route: { pageReference },
} = e.detail;
console.log('navigated to page reference:', pageReference);
}
errorNavigate(e: CustomEvent): void {
const { code, message } = e.detail;
console.error(`navigation error -> ${code}: ${message}`);
}
}
A router container requires a router, and fires these events:
-
onhandlenavigation
: dispatched whennavigate(pageRef)
is called;event.preventDefault()
cancels the navigation event;event.detail
is thePageReference
-
onprenavigate
: dispatched when a navigation event is received and aRouteDefinition
match is found;event.preventDefault()
cancels the navigation event;event.detail
is aRouteChange
-
onpostnavigate
: dispatched when a navigation event has completed;event.detail
is aDomRoutingMatch
for the current location -
onerrornavigate
: dispatched when there is an error processing a navigation event (e.g. noRouteDefinition
match,prenavigate
cancelation);event.detail
is aMessageObject
// router container event payload types
interface DomRoutingMatch {
url: string; // e.g. "/recipes/desserts/010?units=metric&yummy=yes"
route: RouteInstance;
routeDefinition: RouteDefinition;
}
interface RouteChange {
current?: DomRoutingMatch; // the current location info
next: DomRoutingMatch; // location info for the incoming nav event
}
interface MessageObject {
code: string | number;
message: string;
level: number; // Fatal = 0, Error = 1, Warning = 2, Log = 3
}
See a simple routing recipe here.
A router container can have up to 1 child router. Each router is responsible for processing the navigation events from its descendants. Every RouteDefinition
resolving to a view component which includes a child router must set exact
to false
:
// parent RouteDefinition for a page which includes a child router
{
id: 'root',
uri: '/parent/path',
exact: false, // allow the parent and child router to resolve a URI together (i.e. "/parent/path/child/path¶ms)
page: { type: 'home' },
handler: () => import('my/someHandler'), // resolves a view containing a child router container
}
See a nested routing recipe here.
It is the router's job to resolve view components for a given location, and it is the outlet's job to display those view components:
<!-- my/app/app.html -->
<template>
<lwr-router-container>
<lwr-outlet refocus-off onviewchange="{onViewChange}" onviewerror="{onViewError}">
<div slot="error">View component cannot display</div>
</lwr-outlet>
</lwr-router-container>
</template>
The outlet uses the CurrentView
wire to get the current view component, then displays it in the DOM. It has:
-
-
view-name
: the key of theViewSet
entry to display; the default value is"default"
-
refocus-off
boolean: if present, the outlet will not put the browser focus on the view component when it loads; refocusing is on by default as an accessibility feature
-
-
-
onviewchange
event: dispatched whenever the view component changes;event.detail
is the view component class:
type Constructor<T = object> = new (...args: any[]) => T; interface Constructable<T = object> { constructor: Constructor<T>; } interface ViewChangePayload { detail: Constructable; }
-
onviewerror
event: dispatched whenever the view component fails to mount;event.detail
is the error and stack:
interface ViewErrorPayload { detail: { error: Error; stack: string; }; }
-
-
- "error": The contents of the error slot are shown whenever the view component fails to mount
See the RFC for the outlet here and the
viewchange
andviewerror
events here.
A RouteHandler
may return multiple views:
import type { Module, RouteHandlerCallback } from 'lwr/router';
export default class HomeHandler {
callback: RouteHandlerCallback;
constructor(callback: RouteHandlerCallback) {
this.callback = callback;
}
dispose(): void {}
update(): void {
this.callback({
viewset: {
// return multiple views
default: (): Promise<Module> => import('my/home'),
nav: (): Promise<Module> => import('my/homeNav'),
footer: (): Promise<Module> => import('my/homeInfo'),
},
});
}
}
Multiple outlets can be used to display all the current view components by setting different view-names
:
<!-- my/app/app.html -->
<template>
<lwr-router-container>
<lwr-outlet view-name="nav"></lwr-outlet>
<lwr-outlet><!-- default view --></lwr-outlet>
<lwr-outlet view-name="footer"></lwr-outlet>
</lwr-router-container>
</template>
The lwr/navigation
module provides wire adapters from which components can receive information about navigation events.
Get the current page reference from the router container:
import { LightningElement, wire } from 'lwc';
import { CurrentPageReference } from 'lwr/navigation';
export default class Example extends LightningElement {
// Subscribe to page reference updates
@wire(CurrentPageReference)
printPageName(pageRef) {
console.log(`Page name: ${pageRef ? pageRef.attributes.name : ''}`);
}
}
Note: This wire is also available in Lightning Experience.
Get a reference to the current view component. The viewName
configuration property is optional, and falls back to "default"
if unspecified.
import { LightningElement, wire } from 'lwc';
import { CurrentView } from 'lwr/navigation';
export default class MyFooter extends LightningElement {
// Subscribe to view component updates
@wire(CurrentView, { viewName: 'footer' })
viewCtor;
}
Get a reference to a component's navigation context (i.e. its closest ancestor router container), for use with the navigation APIs:
import { LightningElement, wire } from 'lwc';
import { NavigationContext } from 'lwr/navigation';
import type { ContextId } from 'lwr/navigation';
export default class Example extends LightningElement {
@wire(NavigationContext as any)
navContext?: ContextId;
}
Navigate programmatically:
import { LightningElement, api, wire } from 'lwc';
import { NavigationContext, navigate } from 'lwr/navigation';
import type { ContextId } from 'lwr/navigation';
export default class AboutLink extends LightningElement {
@wire(NavigationContext as any)
navContext?: ContextId;
handleClick(event: Event): void {
event.preventDefault();
if (this.navContext) {
navigate(this.navContext, {
type: 'named_page',
attributes: { name: 'about' },
});
}
}
}
Generate a URL for a page reference:
import { LightningElement, api, wire } from 'lwc';
import { NavigationContext, generateUrl } from 'lwr/navigation';
import type { ContextId, PageReference } from 'lwr/navigation';
export default class UrlGenerator extends LightningElement {
@api pageReference?: PageReference;
@wire(NavigationContext as any)
navContext?: ContextId;
connectedCallback(): void {
if (this.pageReference && this.navContext) {
const url = generateUrl(this.navContext, this.pageReference);
console.log(`"${url}" is the URL for this page reference:`, this.pageReference);
}
}
}
The @lwrjs/router
package provides an implementation of lightning/navigation
, which defines the NavigationMixin
and the CurrentPageReference
wire adapter. This allows a component to be written once and plugged in anywhere that supports the lightning/navigation
contracts.
The lightning/navigation
module is an alias for the lwr/navigation
module, so it includes the same wires and APIs, along with the NavigationMixin
.
Important Pick either
lightning/navigation
orlwr/navigation
to use throughout your app. Otherwise, there could be JavaScript bundling clashes when running inprod
mode.
Some developers may prefer to use the NavigationMixin
over the navigate()
and generateUrl()
APIs. Both offer the same functionality, but only the NavigationMixin
is compatible with Lightning Experience (LEX). So a developer writing a component for use in both LWR and LEX should choose the NavigationMixin
.
import { LightningElement } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
const pageRef = {
type: 'standard__recordPage',
attributes: {
recordId: '001xx000003DGg0AAG',
objectApiName: 'Account',
actionName: 'view',
},
};
export default class Example extends NavigationMixin(LightningElement) {
// Navigate to a page reference
navPageRef() {
this[NavigationMixin.Navigate](pageRef);
}
// Generate a URL for a page reference
getUrl() {
this[NavigationMixin.GenerateUrl](pageRef).then((url) => console.log(url));
}
}