Cookbook Router
@cookbook/router-react

Providers and render helpers

Deep reference for RouterProvider, StaticRouterProvider, external-store subscription, React route traversal, route contexts, and route boundaries.

The provider entrypoint is the React rendering layer for Cookbook Router.

It exports:

import {
  OutletContext,
  RouteRenderContext,
  RouterContext,
  RouterProvider,
  SlotRenderContext,
  StaticRouterProvider,
  renderReactRouteMatch,
  renderRouteBoundary,
  useRouterContext,
  useRouterState,
} from '@cookbook/router-react/provider';

It also exports the provider, render-helper, context, and fallback contracts documented on this page.

Use RouterProvider for browser and memory routers. Use StaticRouterProvider only with an already-started static router during SSR or static rendering.

RouterProvider

type RouterScrollBehavior = ScrollBehavior;

interface RouterProviderProps {
  readonly router: Router;
  readonly autoStart?: boolean;
  readonly children?: ReactNode;
  readonly fallback?: ReactNode;
  readonly loadingFallback?: ReactNode;
  readonly errorFallback?: ComponentType<RouterErrorFallbackProps>;
  readonly middleware?: readonly Middleware[];
  readonly scrollRestoration?: boolean;
  readonly scrollBehavior?: RouterScrollBehavior;
}

function RouterProvider(props: RouterProviderProps): ReactElement;

RouterProvider subscribes React to the router with useSyncExternalStore, exposes { router, state } through RouterContext, optionally registers provider middleware, starts the router after mount, coordinates scroll restoration, and renders the active branch.

Prop

Type

Basic browser setup:

import { createRouter } from '@cookbook/router';
import { RouterProvider } from '@cookbook/router-react/provider';
import { routes } from './routes';

const router = createRouter({ routes });

export function App() {
  return (
    <RouterProvider
      router={router}
      fallback={<h1>Not found</h1>}
    />
  );
}

With global fallbacks:

import type { RouterErrorFallbackProps } from '@cookbook/router-react/provider';
import { RouterProvider } from '@cookbook/router-react/provider';

function AppErrorFallback({
  error,
  reset,
  route,
}: RouterErrorFallbackProps) {
  return (
    <main role="alert">
      <h1>Route failed</h1>
      <p>{route ? `Route: ${route.id}` : 'No route matched.'}</p>
      <pre>{String(error)}</pre>
      <button onClick={reset}>Retry</button>
    </main>
  );
}

export function App({ router }: { readonly router: Router }) {
  return (
    <RouterProvider
      router={router}
      fallback={<h1>Not found</h1>}
      loadingFallback={<p>Loading route…</p>}
      errorFallback={AppErrorFallback}
    />
  );
}

With explicit children:

import { Outlet } from '@cookbook/router-react/outlets';
import { RouterProvider } from '@cookbook/router-react/provider';

export function AppShell({ router }: { readonly router: Router }) {
  return (
    <RouterProvider router={router}>
      <header>Dashboard</header>
      <main>
        <Outlet />
      </main>
    </RouterProvider>
  );
}

children replaces the provider's automatic call to renderReactRouteMatch(). Use it only when your shell renders route output itself.

Startup and middleware order

Provider middleware is registered in the same effect that starts the router.

When autoStart is left enabled and the router is not started yet, provider middleware participates in the initial navigation.

<RouterProvider
  router={router}
  middleware={[
    ({ route, redirect }) => {
      if (route.route.meta?.access === 'private') {
        return redirect('/login');
      }
    },
  ]}
/>

When the router is already started and autoStart !== false, provider middleware only affects future transitions. Development builds warn:

Cookbook Router warning: RouterProvider received middleware after the router was already started.
Provider middleware only applies to future navigations in this case.
Remove the manual `await router.start()` before rendering `<RouterProvider />`, or register middleware with `router.useMiddleware(...)` before starting the router and render with `autoStart={false}`.

Correct manual-start pattern:

router.useMiddleware(authMiddleware);
await router.start();

root.render(
  <RouterProvider
    router={router}
    autoStart={false}
  />,
);

If you use autoStart={false} without starting the router yourself, the provider still subscribes and renders current state, but it does not resolve the initial route for you.

Render selection

RouterProvider renders in this order:

StateRendered result
children is providedchildren replaces automatic branch rendering.
Active branch contains a redirect route and no error existsnull while redirect navigation continues.
state.error exists and there is no active matcherrorFallback, or fallback when no error fallback exists.
Active match existsrenderReactRouteMatch(activeMatch, fallback, options).
No active matchfallback ?? null.

For active-match errors, the provider passes state.error into renderReactRouteMatch() so route and layout error ownership can still be respected.

Interception changes the renderable branch. When the current state is intercepted and a previous location exists, the provider renders the previous primary branch while attaching the intercepted output to the active slot state.

Scroll restoration

Provider scroll restoration is opt-in.

<RouterProvider
  router={router}
  scrollRestoration
  scrollBehavior="smooth"
/>

When scrollRestoration is enabled:

  • saved positions are keyed by location.key;
  • known keys restore their saved { x, y } position;
  • new locations without a hash reset to { left: 0, top: 0 };
  • locations with a hash are left to browser/hash behavior;
  • navigation with preventScrollReset bypasses restore/reset;
  • unmount cleanup stores the current window.scrollX and window.scrollY.

scrollBehavior is passed to window.scrollTo() for both saved-position restoration and top resets.

type RouterScrollBehavior = ScrollBehavior;

Common DOM values are:

'auto' | 'instant' | 'smooth'

scrollBehavior does nothing while scrollRestoration is disabled.

Navigation opt-out:

<Link
  to="documents.details"
  params={{ documentId }}
  preventScrollReset
>
  Preview document
</Link>

or programmatically:

await router.navigate.to('documents.details', {
  params: { documentId },
  preventScrollReset: true,
});

During hydration, browsers may have a hash that the server could not serialize. When pathname and search match but the browser hash differs, the provider starts or refreshes the router after layout to synchronize the browser-only hash.

StaticRouterProvider

interface StaticRouterProviderProps {
  readonly router: Router;
  readonly children?: ReactNode;
  readonly fallback?: ReactNode;
  readonly loadingFallback?: ReactNode;
  readonly errorFallback?: ComponentType<RouterErrorFallbackProps>;
  readonly middleware?: readonly Middleware[];
}

function StaticRouterProvider(props: StaticRouterProviderProps): ReactElement;

StaticRouterProvider is the SSR/static wrapper around RouterProvider.

Prop

Type

Static rendering has no React effect phase. This provider never starts the router.

Call await router.start() before rendering:

import { renderToString } from 'react-dom/server';
import { createStaticRouter } from '@cookbook/router';
import { StaticRouterProvider } from '@cookbook/router-react/provider';
import { routes } from './routes';

export async function renderRequest(request: Request) {
  const router = createStaticRouter({
    routes,
    request,
  });

  await router.start();

  return renderToString(
    <StaticRouterProvider
      router={router}
      fallback={<h1>Not found</h1>}
    />,
  );
}

Rendering before startup throws:

Cookbook Router static rendering requires a started router. Call `await router.start()` before rendering `<StaticRouterProvider />`.

StaticRouterProvider delegates to RouterProvider with autoStart={false} and forwards fallback props.

It intentionally has no scrollRestoration or scrollBehavior props. Static history is not the browser.

Request middleware belongs on the static router before startup:

const router = createStaticRouter({
  routes,
  request,
  middleware: [authMiddleware],
});

await router.start();

Passing middleware to StaticRouterProvider is not enough for initial SSR resolution because provider middleware is effect-registered and server rendering has no effect phase.

useRouterState(router)

function useRouterState(router: Router): RouterState;

Subscribes to an explicit router with useSyncExternalStore.

It uses the same router.state getter for client and server snapshots.

import { useRouterState } from '@cookbook/router-react/provider';

export function NavigationBadge({
  router,
}: {
  readonly router: Router;
}) {
  const state = useRouterState(router);

  return <span>{state.navigation}</span>;
}

Use it for integration components that cannot rely on RouterContext.

Most application components should use narrower hooks:

useLocation()
useMatches()
useNavigation()
useRouter()

useRouterState(router) does not start the router and does not install middleware. It only subscribes to state changes.

useRouterContext()

interface RouterContextValue {
  readonly router: Router;
  readonly state: RouterState;
}

function useRouterContext(): RouterContextValue;

Reads RouterContext and returns the provider value.

Prop

Type

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>
    </aside>
  );
}

Outside RouterProvider or StaticRouterProvider, it throws:

Cookbook Router hooks must be used inside <RouterProvider> or <StaticRouterProvider>.

This is a low-level integration hook. Application code should prefer the narrower hooks unless it truly needs both router and full state.

renderReactRouteMatch()

interface RenderReactRouteMatchOptions {
  readonly loadingFallback?: ReactNode;
  readonly errorFallback?: ComponentType<RouterErrorFallbackProps>;
  readonly error?: unknown;
}

function renderReactRouteMatch(
  match: RouteMatch | null | undefined,
  fallback: ReactNode,
  options?: RenderReactRouteMatchOptions,
): ReactNode;

Adapts core renderRouteMatch() traversal to React.

Prop

Type

It:

  • provides route render context;
  • provides outlet render context;
  • provides slot render context for layout views;
  • converts route views to React components;
  • inserts Suspense boundaries;
  • installs route and layout error boundaries;
  • renders configured and call-site intercept output;
  • distinguishes primary no-match fallback from empty slot output.

Low-level render example:

import {
  renderReactRouteMatch,
  type RenderReactRouteMatchOptions,
} from '@cookbook/router-react/provider';

const renderOptions: RenderReactRouteMatchOptions = {
  loadingFallback: <p>Loading…</p>,
  errorFallback: AppErrorFallback,
};

const element = renderReactRouteMatch(
  router.state.match,
  <h1>Not found</h1>,
  renderOptions,
);

With router-state error:

const element = renderReactRouteMatch(
  router.state.match,
  <h1>Not found</h1>,
  {
    error: router.state.error,
    errorFallback: AppErrorFallback,
  },
);

The helper wraps the result in an outer Suspense using:

options.loadingFallback ?? null

More specific route or layout loading fallbacks are installed by per-route boundaries.

A primary no-match returns fallback.

An empty slot, disabled slot, or slot no-match returns null.

When options.error is rendered through a route-state error fallback, the reset callback is a no-op. Provider-level reset behavior belongs to RouterProvider.

renderRouteBoundary()

function renderRouteBoundary(
  match: MatchedRoute,
  element: ReactNode,
  options?: RenderReactRouteMatchOptions,
  fallbacks?: {
    readonly loading?: {
      readonly view: RouteView;
      readonly match: MatchedRoute;
    };
    readonly error?: {
      readonly view: RouteView;
      readonly match: MatchedRoute;
    };
  },
): ReactNode;

Renders one React route boundary around an element.

The value is public. The named fallback interfaces used internally are not exported from @cookbook/router-react/provider, so treat the inline shape above as the public callable contract.

import { renderRouteBoundary } from '@cookbook/router-react/provider';

const element = renderRouteBoundary(
  match,
  <RouteView />,
  {
    loadingFallback: <p>Loading…</p>,
    errorFallback: AppErrorFallback,
  },
);

With inherited route fallbacks:

const element = renderRouteBoundary(
  childMatch,
  <ChildRouteView />,
  {
    loadingFallback: <p>Loading…</p>,
    errorFallback: AppErrorFallback,
  },
  {
    loading: {
      view: ParentLoadingFallback,
      match: parentMatch,
    },
    error: {
      view: ParentErrorFallback,
      match: parentMatch,
    },
  },
);

Boundary behavior:

  • selected loading fallback receives the fallback owner's MatchedRoute;
  • selected error fallback receives the fallback owner's MatchedRoute;
  • global error fallback receives the current matched route when no route/layout error fallback owns the failure;
  • render errors reset when the boundary's match.id changes;
  • slot-isolation context causes route boundaries to rethrow so the nearest <Slot errorFallback> can own the error.

This helper is for custom render adapters. Regular applications should not call it.

Fallback prop contracts

RouteLoadingFallbackProps

interface RouteLoadingFallbackProps {
  readonly route: MatchedRoute;
}

Prop

Type

Route and layout loading fallback components receive the match that owns the selected loading view.

import type { RouteLoadingFallbackProps } from '@cookbook/router-react/provider';

export function RouteLoadingFallback({
  route,
}: RouteLoadingFallbackProps) {
  return <p>Loading {route.id}…</p>;
}

RouteErrorFallbackProps

interface RouteErrorFallbackProps {
  readonly error: unknown;
  readonly reset: () => void;
  readonly route: MatchedRoute;
}

Prop

Type

Route and layout error fallback components receive the match that owns the selected error view.

import type { RouteErrorFallbackProps } from '@cookbook/router-react/provider';

export function RouteErrorFallback({
  error,
  reset,
  route,
}: RouteErrorFallbackProps) {
  return (
    <section role="alert">
      <h2>{route.id} failed</h2>
      <pre>{String(error)}</pre>
      <button onClick={reset}>Retry</button>
    </section>
  );
}

RouterErrorFallbackProps

interface RouterErrorFallbackProps {
  readonly error: unknown;
  readonly reset: () => void;
  readonly route?: MatchedRoute;
}

Prop

Type

route is optional. Startup failures and unmatched router errors may not have an active route.

import type { RouterErrorFallbackProps } from '@cookbook/router-react/provider';

export function AppErrorFallback({
  error,
  reset,
  route,
}: RouterErrorFallbackProps) {
  return (
    <main role="alert">
      <h1>Router error</h1>
      <p>{route ? route.id : 'No active route'}</p>
      <pre>{String(error)}</pre>
      <button onClick={reset}>Retry</button>
    </main>
  );
}

Public context values

The provider entrypoint exports four public React context values.

Most application code should use hooks and components instead. These contexts exist for framework adapters, render integrations, and diagnostics.

RouterContext

interface RouterContextValue {
  readonly router: Router;
  readonly state: RouterState;
}

const RouterContext: Context<RouterContextValue | null>;

Prop

Type

useRouterContext() is the safe public reader. Direct context reads must handle null.

OutletContext

interface OutletContextValue {
  readonly context?: unknown;
}

const OutletContext: Context<OutletContextValue | null>;

Prop

Type

useOutletContext() is the application-facing reader for this context.

RouteRenderContext

interface RouteRenderContextValue {
  readonly match: MatchedRoute;
}

const RouteRenderContext: Context<RouteRenderContextValue | null>;

Prop

Type

This context is installed around route views, layout views, slot views, intercept views, and route fallback views.

SlotRenderContext

interface SlotRenderContextValue {
  readonly ownerRouteId: string;
  readonly slots: Readonly<Record<string, ReactNode>>;
  readonly renderOptions?: RenderReactRouteMatchOptions;
}

const SlotRenderContext: Context<SlotRenderContextValue | null>;

Prop

Type

Slot reads this context to render named slot output.

These implementation contexts are not exported from @cookbook/router-react/provider:

OutletRenderContext
SlotErrorIsolationContext

Their accessor hooks are not public provider-entrypoint exports either:

useOutletContextValue
useOutletRenderContextValue
useRouteRenderContext
useSlotRenderContext

Public export inventory

@cookbook/router-react/provider exports these values:

RouterProvider
StaticRouterProvider
renderReactRouteMatch
renderRouteBoundary
useRouterState

RouterContext
OutletContext
RouteRenderContext
SlotRenderContext
useRouterContext

It exports these public types:

RenderReactRouteMatchOptions
RouteErrorFallbackProps
RouteLoadingFallbackProps
RouterErrorFallbackProps
RouterProviderProps
RouterScrollBehavior
StaticRouterProviderProps

OutletContextValue
RouteRenderContextValue
SlotRenderContextValue
RouterContextValue

The package root re-exports the same public provider surface. The provider subpath is for narrower imports.

Where this bites

Provider middleware after startup is too late for the first route

This is wrong when middleware must affect the initial location:

await router.start();

root.render(
  <RouterProvider
    router={router}
    middleware={[authMiddleware]}
  />,
);

The router already resolved the first route.

Use this:

root.render(
  <RouterProvider
    router={router}
    middleware={[authMiddleware]}
  />,
);

or this:

router.useMiddleware(authMiddleware);
await router.start();

root.render(
  <RouterProvider
    router={router}
    autoStart={false}
  />,
);

The warning is not decorative. Authorization, redirects, rewrites, and cancellation can miss the initial transition.

Static provider middleware is not SSR startup middleware

This is too late for server request resolution:

const router = createStaticRouter({ routes, request });
await router.start();

renderToString(
  <StaticRouterProvider
    router={router}
    middleware={[authMiddleware]}
  />,
);

Register request middleware before startup:

const router = createStaticRouter({
  routes,
  request,
  middleware: [authMiddleware],
});

await router.start();

React effects do not run during renderToString().

Route error fallback and router error fallback are not the same contract

Route fallback:

interface RouteErrorFallbackProps {
  readonly error: unknown;
  readonly reset: () => void;
  readonly route: MatchedRoute;
}

Router fallback:

interface RouterErrorFallbackProps {
  readonly error: unknown;
  readonly reset: () => void;
  readonly route?: MatchedRoute;
}

Provider-level fallback code must handle route being absent.

Render helpers are public, but still low-level

renderReactRouteMatch() and renderRouteBoundary() are exported for custom render integrations. They are not a better RouterProvider.

If an application calls them directly, it owns the wiring that the provider normally handles: subscription, startup, middleware lifecycle, hydration hash sync, scroll restoration, and provider reset behavior.

On this page