Hooks API
Exact overloads, return values, failure behavior, and usage for every public Cookbook Router React hook.
Cookbook Router hooks read resolved router state. They do not parse the URL again, and they do not invent route data after matching has already happened.
Most hooks on this page read RouterContext and throw the missing-provider diagnostic when used outside RouterProvider or StaticRouterProvider.
useOutletContext() is the exception. It reads OutletContext, not RouterContext. Without context it returns undefined, or throws the missing-outlet-context diagnostic when strict: true is used.
The only state subscription that accepts a router directly is useRouterState(router).
Import surface
The hooks entrypoint exports the application-facing hooks:
import {
useBlocker,
useHashParams,
useHref,
useLocation,
useMatches,
useNavigate,
useNavigation,
useOutletContext,
useParams,
useRouteMeta,
useRouter,
useSearchParams,
useUnknownSearchParams,
} from '@cookbook/router-react/hooks';The package root also re-exports them:
import { useNavigate } from '@cookbook/router-react';useRouterContext() is public, but it belongs to the provider surface:
import { useRouterContext } from '@cookbook/router-react/provider';or from the package root:
import { useRouterContext } from '@cookbook/router-react';useRouter()
function useRouter(): Router;Returns the core router from context.
Use it when a component needs an API that does not have a narrower hook: preload(), preloadHref(), refresh(), serialize(), match(), resolve(), or useMiddleware().
import { useRouter } from '@cookbook/router-react/hooks';
export function RefreshButton() {
const router = useRouter();
return (
<button onClick={() => void router.refresh()}>
Refresh route
</button>
);
}Example with manual preloading:
import { useRouter } from '@cookbook/router-react/hooks';
interface WarmUserButtonProps {
readonly slug: string;
}
export function WarmUserButton({ slug }: WarmUserButtonProps) {
const router = useRouter();
return (
<button
onMouseEnter={() => {
void router.preload('users.details', {
params: { slug },
});
}}
onClick={() => {
void router.navigate.to('users.details', {
params: { slug },
});
}}
>
Open user
</button>
);
}Outside RouterProvider or StaticRouterProvider, this hook throws:
Cookbook Router hooks must be used inside <RouterProvider> or <StaticRouterProvider>.useRouterContext()
function useRouterContext(): RouterContextValue;
interface RouterContextValue {
readonly router: Router;
readonly state: RouterState;
}Returns the raw provider value: the router instance and the current router state.
This is the low-level integration hook. Application components normally prefer narrower hooks because they reveal intent and avoid wiring themselves to the full state object.
import { useRouterContext } from '@cookbook/router-react/provider';
export function RouterDebugPanel() {
const { router, state } = useRouterContext();
return (
<aside>
<p>Started: {String(router.started)}</p>
<p>Navigation: {state.navigation}</p>
<p>Location: {state.location.href}</p>
<p>Matched: {state.match?.route.id ?? 'none'}</p>
</aside>
);
}Use this for custom integration components, debugging panels, or wrappers that need both router and state.
Outside RouterProvider or StaticRouterProvider, it throws the missing-provider diagnostic.
useLocation()
function useLocation(): RouterLocation;Returns the current router location.
The returned value includes:
interface RouterLocation {
readonly pathname: string;
readonly search: string;
readonly hash: string;
readonly href: string;
readonly state?: unknown;
readonly key: string;
}Use this when you need the address-like state, not parsed route contracts.
import { useLocation } from '@cookbook/router-react/hooks';
export function CurrentLocation() {
const location = useLocation();
return (
<dl>
<dt>Pathname</dt>
<dd>{location.pathname}</dd>
<dt>Search</dt>
<dd>{location.search || '(none)'}</dd>
<dt>Hash</dt>
<dd>{location.hash || '(none)'}</dd>
<dt>Href</dt>
<dd>{location.href}</dd>
</dl>
);
}Parsed route URL state belongs to:
useParams()useSearchParams()useUnknownSearchParams()useHashParams()
Do not manually parse location.search just to recover typed route state. That already happened before the hook ran.
useMatches()
function useMatches(): readonly MatchedRoute[];Returns the active primary branch from root to leaf.
When no route matches, it returns [].
Each branch entry includes the normalized route and parsed params. Named slot branches are resolved separately and are not appended to this array.
import { useMatches } from '@cookbook/router-react/hooks';
export function MatchTrail() {
const matches = useMatches();
if (!matches.length) {
return <p>No route matched.</p>;
}
return (
<ol>
{matches.map((match) => (
<li key={match.id}>
{match.id}
</li>
))}
</ol>
);
}Example using route metadata:
import { Link } from '@cookbook/router-react';
import { useMatches } from '@cookbook/router-react/hooks';
export function BreadcrumbsFromMatches() {
const matches = useMatches();
const crumbs = matches
.map((match) => match.route.meta?.breadcrumb)
.filter(Boolean);
return (
<nav aria-label="Breadcrumb">
{crumbs.map((crumb) => (
<Link key={crumb.to} to={crumb.to}>
{crumb.label}
</Link>
))}
</nav>
);
}Use useRouteMeta() instead when you need ancestor-aware merge behavior. useMatches() gives you the raw active branch.
useNavigation()
function useNavigation(): RouterNavigationState;Returns the current navigation state from router.state.navigation.
Possible values are:
type RouterNavigationState =
| 'idle'
| 'pending'
| 'redirecting'
| 'blocked'
| 'error';import { useNavigation } from '@cookbook/router-react/hooks';
export function NavigationStatus() {
const navigation = useNavigation();
if (navigation === 'idle') {
return null;
}
return (
<p role="status">
Navigation: {navigation}
</p>
);
}Example with a pending indicator:
import { useNavigation } from '@cookbook/router-react/hooks';
export function TopBarProgress() {
const navigation = useNavigation();
const active =
navigation === 'pending' ||
navigation === 'redirecting';
return active ? (
<div role="status">Loading route…</div>
) : null;
}This hook reports router transition state. It is not a replacement for route-local loading fallbacks.
useNavigate()
function useNavigate(): Router['navigate'];Returns the router's stable programmatic navigation object.
Generated route contracts narrow route IDs, params, search, and hash.
import { useNavigate } from '@cookbook/router-react/hooks';
interface OpenUserProps {
readonly slug: string;
}
export function OpenUser({ slug }: OpenUserProps) {
const navigate = useNavigate();
return (
<button
onClick={() => {
void navigate.to('users.details', {
params: { slug },
});
}}
>
Open user
</button>
);
}to() pushes a new history entry:
import { useNavigate } from '@cookbook/router-react/hooks';
export function CreateDocumentButton() {
const navigate = useNavigate();
return (
<button
onClick={() => {
void navigate.to('documents.create');
}}
>
Create document
</button>
);
}replace() replaces the current history entry:
import { useNavigate } from '@cookbook/router-react/hooks';
interface LoginCompleteProps {
readonly redirectTo: string;
}
export function LoginComplete({ redirectTo }: LoginCompleteProps) {
const navigate = useNavigate();
return (
<button
onClick={() => {
void navigate.replace(redirectTo, {
intercept: false,
});
}}
>
Continue
</button>
);
}back(), forward(), and go(delta) delegate to history:
import { useNavigate } from '@cookbook/router-react/hooks';
export function ModalBackButton() {
const navigate = useNavigate();
return (
<button onClick={() => navigate.back()}>
Close
</button>
);
}Navigation failures are not swallowed. The methods return the same promises as the core router.
useHref()
function useHref<Route extends RouteId>(
routeId: Route,
options?: HrefOptions<Route>,
): string;
function useHref<Route extends RouteId>(
options: NavigateOptions<Route>,
): string;Builds an href through the active router without navigating.
Use it when you need a string for a non-router component, native element, metadata, or custom link wrapper.
import { useHref } from '@cookbook/router-react/hooks';
interface UserHrefProps {
readonly slug: string;
}
export function UserHref({ slug }: UserHrefProps) {
const href = useHref('users.details', {
params: { slug },
});
return <code>{href}</code>;
}The object overload is useful when route ID and URL state are already grouped:
import { useHref } from '@cookbook/router-react/hooks';
export function ReportPermalink() {
const href = useHref({
route: 'reports.month',
params: {
year: 2026,
month: 6,
},
search: {
compare: true,
},
});
return <a href={href}>Copy report link</a>;
}Per-call URL build options are forwarded to router.href():
import { useHref } from '@cookbook/router-react/hooks';
export function FilterHref() {
const href = useHref('products', {
search: {
tags: ['router', 'typescript'],
},
url: {
arrayFormat: 'comma',
},
});
return <code>{href}</code>;
}Missing required params or invalid path constraints throw during render. That is deliberate. Broken URLs should fail where they are declared, not after a user clicks them.
useParams()
function useParams<Route extends RouteId>(
routeId: Route,
): RouteParams<Route>;
function useParams(): RouteParams<RouteId>;Returns parsed path params.
Without a route ID, the hook first uses the current route render context. If no route render context is active, it returns the active match's merged params.
With a route ID, it returns that active branch entry's params. Missing matches or inactive route IDs return an empty object typed to the requested route contract.
import { useParams } from '@cookbook/router-react/hooks';
export function UserDetailsPage() {
const { slug } = useParams('users.details');
return <UserRecord slug={slug} />;
}Built-in numeric constraints are parsed before the hook returns:
import { useParams } from '@cookbook/router-react/hooks';
export function InvoicePage() {
const { id } = useParams('invoices.details');
return <InvoiceRecord id={id} />;
}For a route path like this:
{
id: 'invoices.details',
path: '/invoices/{id:int}',
}id is a number.
When a parent layout needs its own params, pass the parent route ID:
import { Outlet } from '@cookbook/router-react';
import { useParams } from '@cookbook/router-react/hooks';
export function OrganizationLayout() {
const { organizationId } = useParams('organizations');
return (
<section>
<h1>Organization {organizationId}</h1>
<Outlet />
</section>
);
}An inactive route ID returns {}:
const params = useParams('users.details');
// {} when the active branch does not contain "users.details"Do not destructure required fields from an inactive route. Ask for params from the route that is actually active.
useSearchParams()
function useSearchParams<Route extends RouteId>(
routeId: Route,
): RouteSearch<Route>;
function useSearchParams(): RouteSearch<RouteId>;Returns the active match's already-parsed declared search values.
Passing an inactive route ID returns {}.
Defaults, coercion, invalid-search recovery, and array parsing were already applied by URLKit during matching.
import { useSearchParams } from '@cookbook/router-react/hooks';
export function UsersTable() {
const search = useSearchParams('users');
return (
<UserGrid
page={search.page}
q={search.q}
role={search.role}
status={search.status}
/>
);
}Example with array search params:
import { useSearchParams } from '@cookbook/router-react/hooks';
export function ProductFilters() {
const { tags = [] } = useSearchParams('products');
return (
<ul>
{tags.map((tag) => (
<li key={tag}>{tag}</li>
))}
</ul>
);
}The hook does not provide a setter.
Use Link, useNavigate(), useHref(), or the core router API to build a new URL:
import { useNavigate, useSearchParams } from '@cookbook/router-react/hooks';
export function NextUsersPageButton() {
const navigate = useNavigate();
const search = useSearchParams('users');
return (
<button
onClick={() => {
void navigate.to('users', {
search: {
...search,
page: search.page + 1,
},
});
}}
>
Next page
</button>
);
}Hook-level URL options are not supported:
// Wrong. The hook reads already-resolved router state.
useSearchParams('products', {
url: { arrayFormat: 'repeat' },
});Put URL parsing policy on the router, route, static match, or URL-building call.
useUnknownSearchParams()
function useUnknownSearchParams(): RouterUnknownSearchParams;Returns undeclared query-string values preserved by unknownSearch: 'preserve'.
It always returns an object. No active match or no preserved values produces {}.
import {
useSearchParams,
useUnknownSearchParams,
} from '@cookbook/router-react/hooks';
export function CampaignAwareProducts() {
const search = useSearchParams('products');
const unknownSearch = useUnknownSearchParams();
return (
<ProductGrid
page={search.page}
campaign={unknownSearch.utm_campaign}
source={unknownSearch.utm_source}
/>
);
}Declared search belongs to useSearchParams(). Unknown search belongs here.
The two sets are separate so untyped campaign, analytics, or partner parameters cannot masquerade as validated route state.
Example router configuration:
import { createRouter } from '@cookbook/router';
export const router = createRouter({
routes,
url: {
unknownSearch: 'preserve',
},
});Without unknownSearch: 'preserve', undeclared search params are stripped from the resolved match and this hook returns {}.
useHashParams()
type ResolvedRouteHash<Route extends RouteId> =
Exclude<RouteHash<Route>, undefined> | null;
function useHashParams<Route extends RouteId = RouteId>(
routeId?: Route,
): ResolvedRouteHash<Route>;Returns the URLKit-parsed current hash fragment.
It returns null when:
- there is no active match;
- the requested route ID is not active;
- no hash is present.
import { useHashParams } from '@cookbook/router-react/hooks';
export function SettingsTabs() {
const tab = useHashParams('settings');
return (
<Tabs value={tab ?? 'profile'}>
<Tab value="profile">Profile</Tab>
<Tab value="security">Security</Tab>
<Tab value="billing">Billing</Tab>
</Tabs>
);
}For a route hash descriptor like this:
{
id: 'settings',
path: '/settings',
hash: {
type: 'enum',
values: ['profile', 'security', 'billing'],
optional: true,
},
}the hook returns:
'profile' | 'security' | 'billing' | nullHash values are returned without the leading #.
Hook-level URL options are not supported:
// Wrong. The hook reads the already-parsed match.
useHashParams('settings', {
url: { invalidHash: 'error' },
});Configure hash parsing behavior before matching, not inside a component reading match state.
useBlocker()
interface UseBlockerOptions {
readonly when: boolean;
readonly message?: string;
}
interface BlockerState {
readonly blocked: boolean;
}
function useBlocker(options: UseBlockerOptions): BlockerState;Registers a router blocker and browser unload blocker while when is truthy.
When when is falsy, no blocker is registered and the hook returns:
{ blocked: false }When when is truthy:
- in-app router navigation is blocked;
- if
messageandwindow.confirmare available, confirmation decides whether navigation proceeds; - browser unload registers
beforeunload; - browsers control final unload wording;
- unmounting or changing dependencies removes the registrations.
import * as React from 'react';
import {
useBlocker,
useNavigate,
} from '@cookbook/router-react/hooks';
export function MessageComposer() {
const navigate = useNavigate();
const [message, setMessage] = React.useState('');
const { blocked } = useBlocker({
when: Boolean(message),
message:
'Your message has not been sent. Leave this page and discard your draft?',
});
return (
<form>
<textarea
value={message}
onChange={(event) => setMessage(event.currentTarget.value)}
/>
{blocked ? (
<p>You have an unsent draft.</p>
) : null}
<button
type="button"
onClick={() => navigate.back()}
>
Close
</button>
</form>
);
}blocked reflects whether blocking is currently enabled. It is not a historical record of the last transition.
When no message is supplied, router navigation is blocked without a confirmation prompt.
useOutletContext()
interface OutletContextOptions {
readonly strict?: boolean;
}
function useOutletContext(): unknown;
function useOutletContext<Route extends RouteId>(
routeId: Route,
options?: OutletContextOptions,
): RouteOutletContext<Route>;
function useOutletContext<Context>(
options?: OutletContextOptions,
): Context;Reads context supplied by the nearest Outlet or Slot.
The route-ID overload narrows through generated outlet-context contracts. The generic overload is useful before generation or for local context shapes.
When context is absent, the default result is undefined.
With strict: true, the hook throws the missing-outlet-context diagnostic. In the route-ID overload, the route ID is included in that diagnostic.
Layout example:
import { Outlet } from '@cookbook/router-react';
interface AccountContext {
readonly organizationId: string;
}
export function AccountLayout() {
const context: AccountContext = {
organizationId: 'acme',
};
return (
<section>
<Outlet context={context} />
</section>
);
}Descendant route example:
import { useOutletContext } from '@cookbook/router-react/hooks';
interface AccountContext {
readonly organizationId: string;
}
export function BillingPage() {
const context = useOutletContext<AccountContext>({
strict: true,
});
return (
<h1>
Billing for {context.organizationId}
</h1>
);
}Route-ID overload example:
import { useOutletContext } from '@cookbook/router-react/hooks';
export function AccountChildPage() {
const context = useOutletContext('account', {
strict: true,
});
return <p>{context.organizationId}</p>;
}Slot context works the same way:
import { Slot } from '@cookbook/router-react';
export function ShellLayout() {
return (
<>
<main>
<Outlet />
</main>
<Slot
name="modal"
context={{ source: 'shell' }}
/>
</>
);
}A component rendered inside that slot can read the provided context with useOutletContext().
useRouteMeta()
interface UseRouteMetaOptions {
readonly includeAncestors?: boolean;
readonly merge?: false | RouteMetaMergeInput;
}Public overloads:
function useRouteMeta(): RegisteredRouteMeta<RouteId>;
function useRouteMeta(
options: UseRouteMetaOptions & {
readonly merge?: undefined | RouteMetaMergeInput;
},
): RegisteredRouteMeta<RouteId>;
function useRouteMeta(
options: UseRouteMetaOptions & {
readonly merge: false;
},
): readonly RegisteredRouteMeta<string>[];
function useRouteMeta<Route extends string>(
routeId: Route,
): RegisteredRouteMeta<Route>;
function useRouteMeta<Route extends string>(
routeId: Route,
options: UseRouteMetaOptions & {
readonly merge?: undefined | RouteMetaMergeInput;
},
): RegisteredRouteMeta<Route>;
function useRouteMeta<Route extends string>(
routeId: Route,
options: UseRouteMetaOptions & {
readonly merge: false;
},
): readonly RegisteredRouteMeta<Route>[];Without a route ID, the hook prefers the current render-context match. Outside a route view, it uses the active leaf.
With a route ID, an active branch entry is used when available. Otherwise the normalized route tree is queried.
includeAncestors: falseis the default and selects one route.includeAncestors: truebuilds root-to-target metadata.- omitted
mergereturns one merged object using core defaults. merge: falsereturns ordered metadata objects without composition.- a merge string or object forwards to
mergeRouteMetaChain().
Unknown targets resolve to {} or [], not an exception.
Local metadata example:
import { useRouteMeta } from '@cookbook/router-react/hooks';
export function PageTitle() {
const meta = useRouteMeta();
return <h1>{meta.title ?? 'Untitled'}</h1>;
}Target route example:
import { useRouteMeta } from '@cookbook/router-react/hooks';
export function UsersSectionLabel() {
const meta = useRouteMeta('users');
return <span>{meta.title ?? 'Users'}</span>;
}Ancestor-aware breadcrumbs with custom merge behavior:
import { Link } from '@cookbook/router-react';
import { useRouteMeta } from '@cookbook/router-react/hooks';
export function Breadcrumbs() {
const meta = useRouteMeta({
includeAncestors: true,
merge: {
keys: {
breadcrumb: 'append',
},
},
});
const breadcrumbs = meta.breadcrumb ?? [];
return (
<nav aria-label="Breadcrumb">
{breadcrumbs.map((item) => (
<Link key={item.to} to={item.to}>
{item.label}
</Link>
))}
</nav>
);
}Ordered metadata without merging:
import { useRouteMeta } from '@cookbook/router-react/hooks';
export function RawMetaChain() {
const chain = useRouteMeta({
includeAncestors: true,
merge: false,
});
return (
<pre>
{JSON.stringify(chain, null, 2)}
</pre>
);
}Leaf-wins merge mode:
import { useRouteMeta } from '@cookbook/router-react/hooks';
export function ChromeMode() {
const meta = useRouteMeta({
includeAncestors: true,
merge: 'leaf',
});
return meta.chrome?.sidebar === false ? null : (
<AppSidebar />
);
}Where this bites
These hooks subscribe to state; they do not reinterpret it.
If arrayFormat, unknownSearch, defaults, invalid search recovery, invalid hash recovery, path matching, or path constraints are wrong, fixing them inside a component is too late.
Wrong:
// This API does not exist.
const search = useSearchParams('products', {
url: { arrayFormat: 'comma' },
});Right:
import { createRouter } from '@cookbook/router';
export const router = createRouter({
routes,
url: {
arrayFormat: 'comma',
unknownSearch: 'preserve',
},
});Or put the policy on the route:
import { defineRoute } from '@cookbook/router';
export const productsRoute = defineRoute({
id: 'products',
path: '/products',
search: {
tags: {
type: 'string',
many: true,
optional: true,
},
},
url: {
arrayFormat: 'comma',
},
} as const);A hook with a route ID does not activate that route.
const params = useParams('users.details');If users.details is not in the active branch, params is {}. The hook is a reader, not a matcher.
useOutletContext() is not a router-state hook. It only sees the nearest Outlet or Slot context.
const context = useOutletContext<AccountContext>();Without an outlet or slot context, this returns undefined.
Use strict mode when absence is a bug:
const context = useOutletContext<AccountContext>({
strict: true,
});Most other hooks require a router provider. Rendering them outside the provider is not a degraded mode. It is an application wiring error.
// Throws.
function OutsideProvider() {
const router = useRouter();
return <button onClick={() => void router.refresh()} />;
}Outlets, slots, and lazy views
Exact component contracts for nested branch output, outlet context, named slots, slot-local error isolation, and preloadable lazy route views.
React contracts
Public type contracts, context values, provider props, link props, outlet props, fallback props, hook options, and generated registration types exported by @cookbook/router-react.