Skip to main content

Routing

Kog ships a React Router-shaped router in @kog/router. If you know <Routes>/<Route>/<Outlet>, you already know this API.

import { Router, Routes, Route, Outlet } from '@kog/router';

export default function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Shell />}>
<Route index element={<Home />} />
<Route path="detail/:id" element={<Detail />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Router>
);
}

A layout route renders its children through <Outlet />, so shared chrome is written once and stays mounted while the content below it swaps:

function Shell() {
return (
<View style={styles.shell}>
<Text style={styles.title}>My App</Text>
<Outlet />
</View>
);
}

<Router> is an alias for <MemoryRouter> — there is no URL bar on a device, so history lives in memory. Both names work.

const navigate = useNavigate();

navigate('/detail/42'); // push
navigate('/login', { replace: true }); // replace the current entry
navigate('/detail/42', { state: { from: 'home' } });
navigate(-1); // back
navigate('edit'); // relative to the current route
navigate('..'); // climb one route
navigate('..', { relative: 'path' }); // remove one URL segment

Or declaratively:

<Link to="/detail/42"><Text>Open</Text></Link>

<NavLink to="/settings" style={({ isActive }) => [styles.tab, isActive && styles.tabOn]}>
<Text>Settings</Text>
</NavLink>

{!user && <Navigate to="/login" replace />}

Reading the route

const params = useParams<{ id: string }>(); // { id: '42' }; absent optional params are undefined
const location = useLocation(); // { pathname, search, hash, state, key }
const [search, setSearch] = useSearchParams(); // search.get('q')
const match = useMatch('/detail/*'); // PathMatch | null

Path patterns support static segments, :param, optional :param?, a trailing * splat, index routes, and caseSensitive. Matches are ranked by specificity, so /u/new wins over /u/:id regardless of declaration order.

What's different from the web

A top-level route owns a real LVGL screen. Moving between depth-0 routes is a native lv_screen load, not a JS re-render. Routes nested under one render in place through <Outlet /> — that is why the shell above never rebuilds. A screen is a display root created with no parent, so a nested route's element can never be one; the boundary is LVGL's object model, not a policy. Use <Routes screen={false}> to render every level in place, e.g. for a sub-router inside a panel.

navigate(-1) rebuilds, it does not restore. Kog keeps no stack of live screens — navigating away disposes the route and destroys its widgets, which is what bounds memory on a microcontroller. Going back re-matches the previous location and builds it fresh, so widget state from that visit is gone. Lift anything that must survive into state above the route.

Chrome must live in a layout route. Because <Routes> owns the display, a widget rendered as its sibling is built but never shown. The router detects this at mount and throws — put shared chrome in a layout route with <Outlet />.

Route tables are static. <Route> registers during the children pass, which is sealed once <Routes> has built. A conditional or mapped route table is an error rather than a table that silently stops matching; render the full table and guard inside the route element instead.

Failed navigation is transactional. If the replacement route throws while building, its partial widgets are destroyed and the PUSH, REPLACE, or back/forward operation is rolled back. The previous route stays mounted with the same location and history cursor.

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 is auto-lowered to a live memo. Both React Router idioms behave the way you expect:

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. useSearchParams() returns a live view, so search.get('q') is reactive as written, and values derived through it are kept live by the same lowering. An impure derivation from a router hook is a loud compile error, never a silent freeze.

Lifecycle

Navigating away disposes the route's scope, so a useEffect cleanup is your "screen went away" hook — stop timers and sensor polling there. useFocusEffect and useIsFocused exist for React Navigation-shaped source, but in Kog focus and mount are the same event: a route that is not focused is not mounted.

If a route element throws while building, its partial widgets and screen are torn down and the previous route is left intact; the error surfaces at the nearest <ErrorBoundary>.

Not included

createMemoryRouter/RouterProvider, loaders and actions, useRoutes, BrowserRouter/HashRouter, Form, and ScrollRestoration. Screen transition animations are not wired yet — screenLoad always runs with anim=0, ms=0.

See the router API reference for exact signatures.