Router
Everything here comes from @kog/router. It is a strict subset of React Router
v7's declarative API — see the Routing guide for the
model, and the notes below for exact signatures.
Components
| Component | Props | Notes |
|---|---|---|
<Router> / <MemoryRouter> | initialEntries?: To[], initialIndex?: number | The router root. Router is an alias; history is in memory (no URL bar). Defaults to ["/"]. |
<Routes> | location?, screen?: boolean | Matches the current location (or the scoped override) against its <Route> children. Descendant location hooks see the override. screen defaults to true: the depth-0 match owns an LVGL screen. Set false to render every level in place. |
<Route> | path?, index?, caseSensitive?, element?, Component?, children? | Config only — renders nothing itself. element={<Home />} is the canonical form; Component={Home} also works. |
<Outlet> | context?: unknown | Renders the next matched level into this position. context is read by useOutletContext(). |
<Navigate> | to, replace?, state?, relative? | Redirects on mount. A cycle of 10 hops throws with the offending chain. |
<Link> | to, replace?, state?, relative?, disabled?, style? | A Pressable that navigates on press. |
<NavLink> | <Link> props plus end?, caseSensitive? | style may be a function receiving { isActive }; it re-runs on every navigation. Matching is prefix-based by default; end requires an exact match. A link to / is exact automatically. |
Hooks
| Hook | Returns | Notes |
|---|---|---|
useNavigate() | NavigateFunction | navigate(to, opts?) or navigate(delta). Safe to call from an event handler. |
useLocation<S>() | Location<S> | { pathname, search, hash, state, key }. Live inline; body derivations (const { pathname } = useLocation()) are kept live by the compiler. |
useParams<P>() | Readonly<P> | Matched params, ancestors merged in. Values are strings when present; an absent optional param is undefined. Safe as a snapshot (see below). |
useSearchParams(init?) | [SearchParams, SetSearchParams] | A live view of the query string, so search.get('q') is reactive as written. |
useMatch(pattern) | PathMatch | null | The "is this tab active?" primitive. Live inline or as a compiler-lowered body derivation. |
useResolvedPath(to, opts?) | Path | Resolves to exactly as navigate would. |
useOutletContext<T>() | T | The value the parent route passed via <Outlet context>. |
useNavigationType() | 'PUSH' | 'REPLACE' | 'POP' | How the current location was reached. |
useInRouterContext() | boolean | For library code that must degrade outside a router. |
NavigateFunction
navigate(to: To, options?: { replace?: boolean; state?: unknown; relative?: 'route' | 'path' }): void
navigate(delta: number): void
relative defaults to 'route': a relative to resolves against the current
route's matched base, so navigate('edit') from /u/7 under
<Route path="u/:id"> lands on /u/7/edit. A target with no pathname (such as
'?sort=asc') keeps the current location's pathname. The difference between
'route' and 'path' matters for ..: route mode climbs one route, even when
its pattern consumed several URL segments; path mode removes one URL segment
from the same route base.
Path patterns
| Pattern | Matches |
|---|---|
/users | exactly /users |
/users/:id | /users/7 → { id: '7' } |
/users/:id? | /users and /users/7 |
/:lang?/about | /about and /en/about |
/files/* | /files/a/b.txt → { '*': 'a/b.txt' } |
index | the parent route's own path |
Branches are ranked by specificity, not declaration order: a static segment
beats a dynamic one, which beats a splat. So /u/new wins over /u/:id however
you order them. Ties break by declaration order.
Params are decoded with decodeURIComponent, falling back to the raw segment if
it is malformed. Matching is case-insensitive unless caseSensitive is set.
Pure helpers
matchPath, matchRoutes, resolvePath, createPath, parsePath and
generatePath are exported as plain functions. They need no host, so they are
usable (and testable) outside a component.
Reactivity
Location-derived data stays live along two compiler paths: any prop expression
containing a call is bound as a dynamic thunk, and a component-body binding
derived from a router hook (useLocation, useMatch, useResolvedPath,
useNavigationType, useOutletContext, useSearchParams) is auto-lowered to a
live memo:
const { pathname } = useLocation();
<Text>{pathname}</Text> // live — lowered to a memo read
<Text>{useLocation().pathname}</Text> // live — dynamic thunk
useParams() needs no lowering — params are part of a route level's rebuild
key, so the subtree remounts when they change and a snapshot is always current.
An impure derivation from a router hook is a loud compile error, never a silent
freeze.
Board size budget
The router is the largest single thing you can add to an app, so it interacts with your board's app-size ceiling. Measured 2026-08-02, compiled bytecode:
| App | Bytecode |
|---|---|
| A typical non-router example | 30–38 KB |
Two routes, useNavigate + useParams | ~56 KB |
examples/router-demo (nested routes, NavLink, params) | ~69 KB |
The board advertises what it accepts and the CLI/Studio refuse an over-cap app up front with the real number — you get an actionable error, never a blank screen:
| Board | Ceiling (hot reload / install) |
|---|---|
| Waveshare ESP32-S3 1.46B (PSRAM) | 256 KB — every router app fits |
| ESP32-2432S028R "CYD" (no PSRAM) | 76 KB — the full router-demo fits (~69 KB on the wire) |
The CYD hot-reload ceiling is the 96 KB JS arena minus the relocator's scratch reserve. The incoming bytecode itself streams to an alternating flash scratch region, so no bundle-sized internal-RAM buffer competes with the app. Production still gains the dev-link's small RX/task allocations back.
Not included
createMemoryRouter / <RouterProvider>, loaders and actions, useRoutes (it
must return an element, but a Kog component builds into a parent handle),
BrowserRouter / HashRouter, useHref, Form, and ScrollRestoration. Route
tables must be static. Screen transition animations are not wired yet.