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.
<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.
"/"; // exact"/posts/:slug"; // one param"/users/:id/posts/:p"; // several params"/files/*splat"; // wildcardType-safe routes
Augment RegisterRoute and navigate.push, useParams, and the other hooks check against it. Map a route without params to undefined.
declare module "@flemo/react" { interface RegisterRoute { "/": undefined; "/posts/:slug": { slug: string }; }}navigate.push("/posts/:slug", { slug: "hello" }); // oknavigate.push("/posts/:slug", { id: "1" }); // type errornavigate.push("/unknown"); // type errordeclare 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
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.
<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.
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.
// 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 RegisterRouteisnever, sopushaccepts no path. RegisterRouteis 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
Routein the target Router declares mounts nothing. - Params the pattern does not consume become the query string.
- There is no per-
Routetransition. The animation is chosen by each navigation, not by the destination. - If a Router without a
Slothas children that are notRoutes, development reports it. Wrap the routes in aSlot.
Registering animations
transitionstakescreateTransitionorcreateRawTransitionoutput.decoratorstakescreateDecoratororcreateRawDecorator,partTransitionstakescreatePartTransitionorcreateRawPartTransition, andmorphTransitionstakescreateMorphTransitionorcreateRawMorphTransition.- 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
Partselects a part transition by name, whatever screen transition is running. AMorphselects 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. initPathmay 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
classNameandstyleapply 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
routertargets 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
nameprop itself stays a plain string, just asRoute'spathis not checked againstRegisterRoute. 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.stateunder, 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.
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>;