Reference
lumiose/router
The client router for spa routes: links, navigation, reactive route state, persistent layouts, transitions and scroll restoration.
lumiose/router works inside spa routes (export const mode = "spa", or lumiose({ mode: "spa" })
for the whole project); <Link> works on MPA pages too (see Link). Pages and layouts of an SPA import it like any module; on the server
(ssr pages, rendered per request or prerendered at build time) the same names read the request: params(), route(), url() and searchParams() return the
request's values, <Link> renders its <a>, isNavigating() is false, and navigate() and
searchParams.set() throw (call them from event handlers and effects). Concepts:
Routing, Rendering modes.
#Components
#Link
function Link(props: LinkProps): JSX.Element;
type LinkProps = Omit<JSX.IntrinsicElements["a"], "href" | "children"> & {
href: string;
/** Default "hover" in spa pages; on MPA pages, lumiose({ prefetch }). */
prefetch?: "hover" | "viewport" | "none";
children?: JSX.Element;
};An <a href> with every attribute and event of <a>. The router navigates plain <a> links too;
<Link> adds:
- Prefetch. It loads the target route's modules ahead of the click: when the pointer enters
the link or it gets focus (
hover), when it enters the viewport (viewport), or not (none). Data still loads when the page renders.spapages get no speculation rules, so this is the prefetch of their links. - On MPA pages (static and server routes) there is no router:
<Link>renders a plain<a>, outside islands without any JavaScript, andprefetchsets how the page's speculation rules fromlumiose({ prefetch })treat it (Routing). The<a>getsdata-l-prefetch="<level>"; the rules skipnonelinks, prefetchviewportlinks witheagerandhoverlinks withmoderateeagerness, and give unmarked links (<Link>withoutprefetch, plain<a>) the project default. Withlumiose({ prefetch: "none" }), only pages that havehoverorviewportlinks get rules. Browsers prefetch URLs: anonelink to a URL that another link on the page prefetches does not stop it. aria-current="page"while its path is the current URL's path (style it witha[aria-current="page"]).
Build href with href() so the route and its params type-check.
#Navigation
#navigate
function navigate(href: string | URL, options?: NavigateOptions): Promise<void>;
interface NavigateOptions {
/** Replace the current history entry instead of adding one. */
replace?: boolean;
}Navigates on the client when href belongs to the current SPA, else loads the document. Resolves
once the new page is on screen (its async content ready), or when another navigation supersedes
it. Browser only. Only http: and https: URLs (relative ones included) navigate: any other
scheme, such as javascript:, throws a TypeError.
#isNavigating
function isNavigating(): boolean;Whether a navigation is in flight: from the click until the new page is on screen. Reactive, and
counted by isBusy() (decision-20).
#Route state
#params
function params<P extends Routes = never>(): RouteParams<P>; // untyped: Record<string, string>The current route's params, reactive: params<"/songs/[id]">().id. Pages also receive them as
props.params.
#route
function route(): Routes | undefined;The matched route pattern, reactive: route() === "/songs/[id]". Compare patterns instead of
matching url().pathname; typegen makes an unknown pattern a type error. undefined on a not-found
page for an unknown URL (a matched route that throws notFound() keeps its pattern).
#url
function url(): URL;The current URL, reactive. Do not mutate it.
#locale
function locale(): Locale;With lumiose({ i18n }), the locale of the current URL (its prefix, else the default locale),
reactive inside SPA routes; in islands of other routes, the page's. undefined without
lumiose({ i18n }), typed string | undefined. A navigation to another locale renders every level again, and <html lang>
follows. See Internationalization.
#searchParams
interface SearchParams {
(): URLSearchParams;
set(
next: SearchParamsInit | ((current: URLSearchParams) => SearchParamsInit),
options?: NavigateOptions,
): Promise<void>;
}
const searchParams: SearchParams;
type SearchParamsInit =
| URLSearchParams
| string
| Record<string, string | number | boolean | readonly string[] | null | undefined>;searchParams() is the current URL's query, reactive (a new object per change). searchParams.set()
navigates to the current path with a new query, keeping the scroll position; null and
undefined values are left out. It adds a history entry unless { replace: true }.
While a page is still loading during a navigation, params(), route(), url() and searchParams() called in it
(and its props) already describe the page it is loading; everything else keeps the shown URL until
the new page is on screen.
#Behaviour
- Interception. The router handles same-document navigations to URLs of its SPA: link clicks,
navigate(), back/forward and (with the Navigation API)GETform submissions. URLs of other routes (staticorserverpages, endpoints, other SPAs) load the document. Without the Navigation API, a History API fallback intercepts link clicks andpopstate; it is a chunk of its own that only those browsers load, shortly after the SPA starts (a click before it loads is a document navigation). - Persistent layouts. A layout's
childrenis the outlet. Layouts the old and the new page share stay mounted with their state; the first level that differs (another file, or other values of the params in its path, such assongs/[id]/page.stsxfor another song) is replaced. A navigation that only changes the query keeps every level and updatessearchParams(). - Transitions. The new page is created off-screen. The old page stays until the new page's
async content (async components and resources outside a
<Suspense>of the new page) is ready; then the URL signals change and the pages swap in one step. A<Suspense>inside the new page shows its fallback instead. - Scroll and focus. After a navigation the page scrolls to the top (or to the URL's
#hash) and focus moves to the document (or the first[autofocus]element); back/forward restore the scroll position.searchParams.set()keeps both. - Errors.
notFound()and unknown URLs render the nearestnot-foundpage of the SPA; other errors itserrorpage ({ status, error }props), with the layouts kept. A thrownredirect()navigates. Without such a page, a minimal built-in error page replaces the SPA. Anot-foundpage that the server rendered at the URL of a prerendered page with params (404.htmlon a static host) renders that page instead: see Prerendered SPA routes. invalidate()fromlumiosere-runs the SPA's async components and resources (they keep their UI meanwhile) and resolves once they settled.submit()redirects navigate through the router, and a successfulsubmit()invalidates (on MPA pages it does not: that would reload).- Stale modules. A page module that fails to load (an old chunk after a deploy) loads the URL as a document instead.
#Example
// src/routes/music/layout.tsx: an SPA under /music, with a persistent layout
import { href, type LayoutProps } from "lumiose";
import { Link, isNavigating } from "lumiose/router";
export const mode = "spa";
export default function Layout(props: LayoutProps<"/music">) {
return (
<div class="app">
<nav>
<Link href={href("/music")}>Songs</Link>
<Link href={href("/music/settings")} prefetch="viewport">
Settings
</Link>
</nav>
{isNavigating() && <progress />}
<main>{props.children}</main>
</div>
);
}// src/routes/music/page.tsx: a filter kept in the query
import { searchParams } from "lumiose/router";
export default function Songs() {
const query = () => searchParams().get("q") ?? "";
return (
<input
type="search"
value={query()}
onInput={(event) =>
searchParams.set({ q: event.currentTarget.value || null }, { replace: true })
}
/>
);
}// src/routes/music/settings/page.tsx
import { href } from "lumiose";
import { navigate } from "lumiose/router";
export default function Settings() {
return <button onClick={() => navigate(href("/music"), { replace: true })}>Done</button>;
}See also: Routing, Rendering modes,
lumiose: app layer.