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.
Navigating
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.