| name | lambda-router |
| description | Guide for writing code that uses @studiolambda/router — a React 19 client-side router built on the browser Navigation API with a trie-based URL matcher. Use when writing React components, pages, layouts, or navigation logic that imports from "@studiolambda/router" or "@studiolambda/router/react". Covers route definitions with createRouter, the Router/Link components, all 16 hooks, middleware, prefetch, lazy loading, SSR with createMemoryNavigation, search params, and form handling.
|
| metadata | {"version":"2.0.0"} |
Lambda Router
@studiolambda/router is a React 19 client-side router built on the browser Navigation API. Two entry points:
@studiolambda/router — framework-agnostic trie-based URL matcher
@studiolambda/router/react — React 19 components, hooks, and navigation
How It Works
The <Router> component intercepts the browser Navigation API's navigate event, matches the destination URL against a trie of registered routes, runs prefetch logic in the precommit phase (before the URL commits to the address bar), then wraps the React state update in startTransition for concurrent rendering. The matched route component renders inside a Suspense boundary wrapped by any middleware components.
Links don't need onClick or preventDefault — the Navigation API intercepts anchor clicks natively.
Quick Start
import { lazy, Suspense } from 'react'
import { createRouter, Router, Link } from '@studiolambda/router/react'
const Home = lazy(() => import('./pages/Home'))
const User = lazy(() => import('./pages/User'))
const router = createRouter((route) => {
route('/').render(Home)
route('/user/:id').render(User)
})
function App() {
return (
<Suspense fallback="Loading...">
<nav>
<Link href="/">Home</Link>
<Link href="/user/42">User 42</Link>
</nav>
<Router matcher={router} />
</Suspense>
)
}
Route Definition — createRouter
Build a Matcher<Handler> with the declarative builder API. See route-builder.md for the full chainable API.
const router = createRouter((route) => {
route('/').render(Home)
route('/user/:id').render(User)
route('/files/*path').render(FileViewer)
route('/old').redirect('/new')
route('/old-user/:id').redirect(({ params }) => `/user/${params.id}`)
route('/search')
.prefetch(({ url }) => prefetchSearchResults(url.searchParams.get('q')))
.scroll('manual')
.formHandler((formData) => handleSearchForm(formData))
.render(SearchPage)
const authed = route().middleware([Auth]).group()
authed('/dashboard').render(Dashboard)
authed('/settings').render(Settings)
const admin = authed('/admin').middleware([AdminGuard]).group()
admin('/users').render(AdminUsers)
admin('/config').render(AdminConfig)
})
Rules:
.render(), .redirect(), .group() are terminal — no further chaining after
- Groups inherit middleware and prefetch from parents; redirects do NOT inherit middleware
- Duplicate route registration throws
- Static redirect cycles are detected at build time
Components
<Router>
Top-level orchestrator. Provides contexts consumed by all hooks.
<Router
matcher={router}
navigation={memoryNav}
notFound={Custom404}
fallback={<Spinner />}
transition={[isPending, startTransition]}
onNavigateSuccess={() => analytics.pageView()}
onNavigateError={(error) => reportError(error)}
/>
Falls back to window.navigation when no navigation prop or context is provided.
<Link>
Anchor element with prefetch and active link detection.
<Link href="/about">About</Link>
<Link href="/about" prefetch="hover">About</Link>
<Link href="/about" prefetch="viewport">About</Link>
<Link
href="/about"
className={({ isActive }) => isActive ? "nav-active" : "nav-link"}
>About</Link>
<Link href="/docs" activeExact={false}>Docs</Link>
Active links get data-active and aria-current="page" attributes automatically.
Hooks
All hooks must be used inside a <Router> tree. They throw descriptive errors outside their provider. See hooks-reference.md for complete API.
| Hook | Returns | Purpose |
|---|
useParams() | Record<string, string> | Dynamic route params (:id segments) |
usePathname() | string | Current URL pathname |
useSearchParams() | [URLSearchParams, setter] | Search params + setter (preserves hash) |
useNavigate() | (url, options?) => NavigationResult | Programmatic navigation |
useNavigation() | Navigation | Raw Navigation API object |
useNavigationType() | NavigationType | null | push/replace/reload/traverse |
useNavigationSignal() | AbortSignal | null | Current navigation's abort signal |
useIsPending() | boolean | Whether a transition is in progress |
useBack() | { back, canGoBack } | Back navigation + reactive availability |
useForward() | { forward, canGoForward } | Forward navigation + reactive availability |
usePrefetch() | (url) => void | Trigger route prefetch manually |
usePrefetchEffect(ref, opts) | void | Attach prefetch to DOM element |
useActiveLinkProps(href, opts) | { isActive, props } | Active link detection |
Common Hook Patterns
function UserPage() {
const { id } = useParams()
return <div>User {id}</div>
}
function LogoutButton() {
const navigate = useNavigate()
return (
<button
onClick={() => {
navigate('/login')
}}>
Logout
</button>
)
}
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams()
const query = searchParams.get('q') ?? ''
return <input value={query} onChange={(e) => setSearchParams({ q: e.target.value })} />
}
function Layout({ children }: { children: React.ReactNode }) {
const isPending = useIsPending()
return (
<div>
{isPending && <ProgressBar />}
{children}
</div>
)
}
Middleware
Middleware components receive { children } and wrap the route component. They can suspend, conditionally render, or add context.
import { type PropsWithChildren, use } from 'react'
function Auth({ children }: PropsWithChildren) {
const session = use(fetchSession())
if (!session) return <LoginPage />
return <SessionContext value={session}>{children}</SessionContext>
}
function DashboardLayout({ children }: PropsWithChildren) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
)
}
const router = createRouter((route) => {
const authed = route().middleware([Auth, DashboardLayout]).group()
authed('/dashboard').render(Dashboard)
})
Middlewares nest outermost-first: [Auth, Layout] means Auth wraps Layout wraps RouteComponent.
SSR and Testing
Use createMemoryNavigation for non-browser environments:
import { createMemoryNavigation, Router } from '@studiolambda/router/react'
const navigation = createMemoryNavigation({ url: 'https://example.com/page' })
function ServerApp() {
return <Router matcher={router} navigation={navigation} />
}
function renderWithRouter(ui: React.ReactNode, { url = '/' } = {}) {
const nav = createMemoryNavigation({ url: `http://localhost${url}` })
return render(
<Router matcher={router} navigation={nav}>
{ui}
</Router>
)
}
createMemoryNavigation provides a stub Navigation with no-op event methods, single-entry history, and pre-resolved navigation promises. back()/forward()/traverseTo() throw "not supported" errors.
URL Pattern Matching (standalone)
The core matcher can be used independently of React:
import { createMatcher } from '@studiolambda/router'
const matcher = createMatcher<string>()
matcher.register('/users', 'list')
matcher.register('/users/:id', 'detail')
matcher.register('/files/*path', 'files')
matcher.match('/users')
matcher.match('/users/42')
matcher.match('/files/a/b/c')
matcher.match('/unknown')
- Matching priority: static > dynamic (
:param) > wildcard (*param)
- Trailing slashes are ignored
- Conflicting param names at the same trie level throw
- Bare
* captures into param named "*"
When to Load References