Skip to content
Ilha
Esc
↑↓navigate↵open⌘Jpreview
On this page

Routes and navigation

Register routes, navigate between them, and read route state.

Create a router, register each URL pattern, then mount it:

import { const router: (options?: RouterOptions) => RouterBuilderrouter } from "@ilha/router";

const const Home: () => ViewHome = () => <JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>Home</JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>;
const const User: () => ViewUser = () => <JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>User</JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>;
const const NotFound: () => ViewNotFound = () => <JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>Not found</JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>;

function router(options?: RouterOptions): RouterBuilderrouter({ RouterOptions.notFound?: Component | undefinednotFound: const NotFound: () => ViewNotFound })
  .
RouterBuilder.route: (pattern: string, page: Page, opts?: {
    layouts?: LayoutHandler[];
}) => RouterBuilder
route
("/", const Home: () => ViewHome)
.
RouterBuilder.route: (pattern: string, page: Page, opts?: {
    layouts?: LayoutHandler[];
}) => RouterBuilder
route
("/user/:id", const User: () => ViewUser)
.RouterBuilder.mount: (target: string | Element, options?: MountOptions) => () => voidmount("#app");

Match route patterns

Pattern Example URL Params
/ / {}
/user/:id /user/42 { id: "42" }
/:org/:repo /ilha/router { org: "ilha", repo: "router" }
/docs/**:slug /docs/guide/intro { slug: "guide/intro" }
/** any unmatched path {}

Static segments win over parameters, then catch-alls.

Use an ordinary <a href>. A mounted SPA router intercepts same-origin clicks:

const const Navigation: () => ViewNavigation = () => (
  <JSX.IntrinsicElements.nav: HTMLAttributes<HTMLElement>nav>
    <JSX.IntrinsicElements.a: AnchorHTMLAttributesa href?: Attrhref="/">Home</JSX.IntrinsicElements.a: AnchorHTMLAttributesa>
    <JSX.IntrinsicElements.a: AnchorHTMLAttributesa href?: Attrhref="/about">About</JSX.IntrinsicElements.a: AnchorHTMLAttributesa>
  </JSX.IntrinsicElements.nav: HTMLAttributes<HTMLElement>nav>
);

Use navigate() after application logic:

import { const navigate: (to: string, opts?: NavigateOptions) => voidnavigate } from "@ilha/router";

function navigate(to: string, opts?: NavigateOptions): voidnavigate("/about");
function navigate(to: string, opts?: NavigateOptions): voidnavigate("/account", { NavigateOptions.replace?: boolean | undefinedreplace: true });
function navigate(to: string, opts?: NavigateOptions): voidnavigate("/results?page=2", { NavigateOptions.scroll?: boolean | undefinedscroll: false });

Share layouts across routes

Pass shared layouts in route() options. Matching layouts stay mounted while you move between their pages, so layout state survives navigation:

import { const router: (options?: RouterOptions) => RouterBuilderrouter } from "@ilha/router";

import type { type View = VNode | UnsafeHtml | JsonText | AtomHandle<unknown> | Stream<View, unknown, unknown> | Component | View[] | Iterable<View>View } from "ilha";

const 
const AppLayout: ({ children }: {
    children?: View;
}) => View
AppLayout
= ({ children: Viewchildren }: { children?: Viewchildren?: type View = VNode | UnsafeHtml | JsonText | AtomHandle<unknown> | Stream<View, unknown, unknown> | Component | View[] | Iterable<View>View }) => <JSX.IntrinsicElements.div: HTMLAttributes<HTMLDivElement>div>{children: Viewchildren}</JSX.IntrinsicElements.div: HTMLAttributes<HTMLDivElement>div>;
const const PageA: () => ViewPageA = () => <JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>A</JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>; const const PageB: () => ViewPageB = () => <JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>B</JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>; function router(options?: RouterOptions): RouterBuilderrouter() .
RouterBuilder.route: (pattern: string, page: Page, opts?: {
    layouts?: LayoutHandler[];
}) => RouterBuilder
route
("/a", const PageA: () => ViewPageA, { layouts?: LayoutHandler[] | undefinedlayouts: [
const AppLayout: ({ children }: {
    children?: View;
}) => View
AppLayout
] })
.
RouterBuilder.route: (pattern: string, page: Page, opts?: {
    layouts?: LayoutHandler[];
}) => RouterBuilder
route
("/b", const PageB: () => ViewPageB, { layouts?: LayoutHandler[] | undefinedlayouts: [
const AppLayout: ({ children }: {
    children?: View;
}) => View
AppLayout
] })
.RouterBuilder.mount: (target: string | Element, options?: MountOptions) => () => voidmount("#app");

Search- or hash-only changes re-render the same page without remounting it. Routes with different layouts remount as before.

Read the current route

import { 
const useRoute: () => {
    hash: typeof routeHash;
    navigating: typeof navigating;
    params: typeof routeParams;
    path: typeof routePath;
    search: typeof routeSearch;
}
useRoute
} from "@ilha/router";
const const UserPage: () => ViewUserPage = () => { const { const path: () => stringpath, const params: () => Record<string, string>params, const search: () => stringsearch } =
function useRoute(): {
    hash: typeof routeHash;
    navigating: typeof navigating;
    params: typeof routeParams;
    path: typeof routePath;
    search: typeof routeSearch;
}
useRoute
();
return ( <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p> {const path: () => stringpath()} {const params: () => Record<string, string>params().stringid} {const search: () => stringsearch()} </JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p> ); };

isActive(href) is true when the current path matches.

Keep UI state in the query string

You bind one query param with searchParam(). Writes replace the history entry, and the page re-renders in place:

import { const searchParam: SearchParamFn
Read and write one query param through the router. Writes `replace` the history entry without scrolling, so the mounted router re-renders the current route in place instead of remounting it.
searchParam
} from "@ilha/router";
const const Tabs: () => ViewTabs = () => { const const tab: SearchParam<string>tab = function searchParam(name: string, opts: SearchParamOptions<string>): SearchParam<string> (+1 overload)
String param: `parse` is optional.
searchParam
("t", { SearchParamOptions<string>.default: string
Value when the param is absent. Writing it removes the param.
default
: "overview" });
return ( <JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton type?: string | undefinedtype="button" EventProps<HTMLButtonElement>.onclick?: ((event: Targeted<HTMLButtonElement, MouseEvent>) => void) | undefinedonclick={() => const tab: SearchParam<string>tab.SearchParam<string>.set: (next: string) => voidset("logs")}> {
const tab: SearchParam
() => string
tab
()}
</JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton> ); };

Writing the default removes the param. Pass parse and serialize for non-string values, such as { default: 1, parse: Number }.

import { head } from "@ilha/router";

export default function About() {
  head({ title: "About" });
  return <h1>About</h1>;
}

Layouts can set titleTemplate. On the client, head() updates document after mount and on navigation. See the router reference.

Fetch first, then describe the page. The head window stays live for the whole mount, so a call after await still updates document — never assign document.title by hand:

import { head } from "@ilha/router";

export default async function Post() {
  const post = await fetchPost();
  head({ title: post.title });
  return <h1>{post.title}</h1>;
}

Skip interception

The mounted router intercepts same-origin <a> clicks. You opt a link out with data-no-intercept:

const LegacyLink = () => (
  <a href="/legacy/report" data-no-intercept>
    Full-page report
  </a>
);

Observe navigation

Export Meaning
beforeNavigate(fn) Hook before every navigation; call cancel() to stop it. Returns an unsubscribe function
afterNavigate(fn) Hook after every navigation. Returns an unsubscribe function
navigating() True while a navigation is in flight
routeHash() Current hash, including the leading #
prime() Sync route state from the address bar without mounting
useContext() { request } for the current render, if the server scoped one

There are no loaders and no route guards. Fetch inside an async page; confirm or cancel through beforeNavigate().

Was this page helpful?