Core

Router and Route

Router manages one screen stack, its history, and its transitions; each Route maps a path pattern to the screen it renders.

2 min read
tsx
<Router>  <Route path="/" element={<Home />} />  <Route path="/posts/:slug" element={<Post />} />  <Route path={["/settings", "/settings/:tab"]} element={<Settings />} /></Router>

Each Route maps a path, or an array of paths, to an element, normally a Screen. Without a , the Router's children are its routes and its screens fill the viewport.

Path patterns

flemo matches paths with path-to-regexp v8.

ts
"/"; // exact"/posts/:slug"; // one param"/users/:id/posts/:p"; // several params"/files/*splat"; // wildcard

Type-safe routes

Augment RegisterRoute and navigate.push, useParams, and the other hooks check against it. Map a route without params to undefined.

ts
declare module "@flemo/react" {  interface RegisterRoute {    "/": undefined;    "/posts/:slug": { slug: string };  }}
tsx
navigate.push("/posts/:slug", { slug: "hello" }); // oknavigate.push("/posts/:slug", { id: "1" }); // type errornavigate.push("/unknown"); // type error

declare module blocks merge, so declare each route at the bottom of its screen's file, not in a central registry. Do the same for RegisterTransition, RegisterDecorator, and RegisterPartTransition.

Router props

PropDefaultWhat it does
defaultTransitionNamecupertinoTransition used when a navigation does not set one
transitions[]Custom screen transitions to register
decorators[]Custom decorators (overlays) to register
partTransitions[]Custom transitions to register
morphTransitions[]Custom transitions to register
historybrowserbrowser (URL, back/forward) or memory (isolated, no URL)
createDrivernoneReplaces the browser history driver
initPath/Start path when the URL is not read: server, nested, or memory
namenoneName that navigation from another Router uses to target this one
strictRoutesfalseMakes the missing-route warning an error
className / stylenoneSize a nested Router's box

Nested and named Routers

A Router inside another is a separate area with its own stack, contained to a box you size. It still uses browser history unless you pass history="memory", for a demo, wizard, or carousel.

Give a Router a name when code in a nested Router must move it, like a card opening a full-screen detail. shows how to target it.

tsx
<Router name="app">  <Route path="/members/:id" element={<Member />} />  <Route path={["/region", "/region/people"]} element={<RegionActivity />} /></Router>;​function RegionActivity() {  return (    <Router name="region" initPath="/region" className="h-full w-full">      <RegionHeader />      <Slot className="h-full w-full">        <Route path="/region" element={<RegionFeed />} />        <Route path="/region/people" element={<RegionPeople />} />      </Slot>    </Router>  );}

Registering names is optional. Once registered, a router target that matches no registered Router name is a compile error.

ts
declare module "@flemo/react" {  interface RegisterRouter {    app: true;    region: true;  }}

Server-side rendering

flemo drives window.history once it mounts. The server has no URL, so pass the first route to render as initPath. A pure SPA (Vite and similar) does not need it.

tsx
// On the server, the root Router renders initPath. On the client it reads the URL.<Router initPath={requestPathname}>  <Route path="/" element={<Home />} />  <Route path="/posts/:slug" element={<Post />} /></Router>

flemo does not work alongside a host framework that also handles routing. Use it as a pure SPA, or as a self-contained client-only area that does not share routing with the host.

Edge casesDetails

Routes

  • Until you augment it, keyof RegisterRoute is never, so push accepts no path.
  • RegisterRoute is one global registry shared by every Router. A path can type-check while the Router you navigate does not declare it. covers what happens then.
  • A navigation to a path that no Route in the target Router declares mounts nothing.
  • Params the pattern does not consume become the query string.
  • There is no per-Route transition. The animation is chosen by each navigation, not by the destination.
  • If a Router without a Slot has children that are not Routes, development reports it. Wrap the routes in a Slot.

Registering animations

  • transitions takes createTransition or createRawTransition output. decorators takes createDecorator or createRawDecorator, partTransitions takes createPartTransition or createRawPartTransition, and morphTransitions takes createMorphTransition or createRawMorphTransition.
  • Registering transitions, decorators, and part transitions compiles their keyframes into the document. Morph transitions compile to no CSS. Registering one only makes its name available.
  • A decorator is used only through a transition's decoratorName, never set on an element.
  • A Part selects a part transition by name, whatever screen transition is running. A Morph selects a morph transition by name.

Where a Router starts

  • Only a root browser Router reads the live URL on the client.
  • On the server, a nested Router, and a memory Router all start from initPath.
  • initPath may carry a query, like /onboarding?step=email. The route matches the pathname and the params still resolve from the query.
  • A root Router renders no wrapper and its screens are fixed to the viewport, so className and style apply only to a nested Router.
  • History mode is independent of nesting. Nesting only controls the contained box.

Names

  • Names must be unique among Routers that enclose one another. A duplicate in one chain is reported in development, because router targets would resolve to the nearer one.
  • Two Routers in different branches may share a name. A lookup only walks the Routers that enclose the call, never a sibling.
  • With an empty RegisterRouter, any string is accepted as a target and a mistyped name is caught in development.
  • The name prop itself stays a plain string, just as Route's path is not checked against RegisterRoute. A declaration has nothing to check against. A reference does.
  • A name is only used to find the Router to navigate. It is not the key flemo stores history.state under, so renaming a Router never disconnects its existing history entries.
  • Because you write the name yourself, it is stable across SSR and hydration.

Custom history driver

createDriver receives the Router's key, used to keep each Router's history.state separate, and returns a HistoryDriver. Wrap createBrowserHistoryDriver to map a URL prefix, such as a locale, while the Router works in unprefixed paths. It is ignored when history is memory.

tsx
import { createBrowserHistoryDriver, type HistoryDriver } from "@flemo/react";​// Keeps a /ko prefix in the URL while the Router sees unprefixed paths.function createPrefixDriver(routerKey?: string): HistoryDriver {  const base = createBrowserHistoryDriver(routerKey);  const strip = (pathname: string) => pathname.replace(/^\/ko(?=\/|$)/, "") || "/";  const add = (url: string) => (url === "/" ? "/ko" : `/ko${url}`);​  return {    ...base,    readPathname: () => strip(base.readPathname()),    pushState: (state, url) => base.pushState(state, add(url)),    replaceState: (state, url) => base.replaceState(state, add(url)),    subscribe: (listener) =>      base.subscribe((event) => listener({ ...event, pathname: strip(event.pathname) }))  };}​<Router createDriver={createPrefixDriver}>{/* routes */}</Router>;
Edit on GitHub