Skip to content

React Integration

olegivanov edited this page May 9, 2026 · 6 revisions

React Integration

@real-router/react — React adapter for Real-Router. Ships three entry points: modern (React 19.2+ with <Activity>), legacy (React 18+), and Ink (terminal UIs).

npm install @real-router/react @real-router/core @real-router/browser-plugin

Peer dependencies:

  • react + react-dom ≥ 18.0.0 (main / legacy)
  • react + react-dom ≥ 19.2.0 (main only — <Activity> API)
  • ink ≥ 7.0.0 (for the /ink entry only, optional)

For other frameworks, see Preact · Solid · Vue · Svelte · Angular · Ink (terminal UI).


Quick Start

import { createRouter } from "@real-router/core";
import { browserPluginFactory } from "@real-router/browser-plugin";
import {
  RouterProvider,
  RouteView,
  Link,
  useRouteNode,
} from "@real-router/react";

const router = createRouter([
  { name: "home", path: "/" },
  {
    name: "users",
    path: "/users",
    children: [
      { name: "list", path: "/list" },
      { name: "profile", path: "/:id" },
    ],
  },
]);

router.usePlugin(browserPluginFactory());
router.start();

function App() {
  return (
    <RouterProvider router={router}>
      <nav>
        <Link routeName="home">Home</Link>
        <Link routeName="users">Users</Link>
      </nav>
      <RouteView nodeName="">
        <RouteView.Match segment="home">
          <HomePage />
        </RouteView.Match>
        <RouteView.Match segment="users">
          <UsersLayout />
        </RouteView.Match>
        <RouteView.NotFound>
          <NotFoundPage />
        </RouteView.NotFound>
      </RouteView>
    </RouterProvider>
  );
}

function UsersLayout() {
  const { route } = useRouteNode("users");
  if (!route) return null;
  return <div>Active: {route.name}</div>;
}

Entry Points

Five entry points via package.json exports (plus a react-server condition on . and ./ssr):

Entry point Import path React version Runtime What's included
Main @real-router/react 19.2+ DOM Full client API including RouteView with keepAlive (React <Activity>). No SSR-feature components — those live at /ssr
SSR @real-router/react/ssr 19.2+ DOM (SSR-aware) <ClientOnly>, <ServerOnly>, <Await>, <Streamed>, useDeferred — components/hooks that participate in the SSR pipeline
Legacy @real-router/react/legacy 18+ DOM Client API for React 18 (no RouteView, no SSR helpers)
Legacy SSR @real-router/react/legacy/ssr 18+ DOM (SSR-aware) SSR-feature subset for React 18 — same as /ssr minus <Await> (which depends on React 19's use(promise))
Ink @real-router/react/ink 19.2+ + Ink 7+ Terminal InkRouterProvider + InkLink + hooks; no Link, no RouteView, no announceNavigation
// React 19.2+ (recommended) — client-only API
import { RouterProvider, useRouteNode, Link, RouteView } from "@real-router/react";

// React 19.2+ — SSR-feature components/hooks
import { ClientOnly, ServerOnly, Await, Streamed, useDeferred } from "@real-router/react/ssr";

// React 18+ — client-only API
import { RouterProvider, useRouteNode, Link } from "@real-router/react/legacy";

// React 18+ — SSR-feature subset (no <Await> — uses React 19's use())
import { ClientOnly, ServerOnly, Streamed, useDeferred } from "@real-router/react/legacy/ssr";

// Terminal UIs via Ink v7+
import { InkRouterProvider, useRouteNode, InkLink } from "@real-router/react/ink";

Decision matrix:

  • Touching the SSR pipeline (<Suspense> + defer() consumers, hydration-aware boundaries)? → import from /ssr.
  • On React 18 or unable to upgrade? → use /legacy for client API + /legacy/ssr for SSR helpers.
  • Building a CLI / terminal UI? → use /ink. See Ink Integration.
  • New project or already on React 19.2+? → main entry + /ssr as needed.

The main entry under the react-server condition (Vite RSC, Webpack RSC, Turbopack, Parcel) resolves to a type-only entry — Server Components can import public API types without pulling client-only code into the server bundle. Per-request data fetching lives in @real-router/rsc-server-plugin, not the main entry. See RSC Integration.

All entries share the same underlying source — switching between them is a purely mechanical import swap.


Key Features

Native useSyncExternalStore

React 18+ ships useSyncExternalStore natively. Hooks like useRouteNode delegate to it directly — no polyfill, no custom subscription plumbing. This is StrictMode-safe: double-mount in development reuses the shared subscription from @real-router/sources.

<Activity> for keepAlive (React 19.2+)

RouteView.Match supports a keepAlive prop on React 19.2+. Unlike Solid/Svelte/Preact where inactive routes unmount, keepAlive preserves the component tree via React's <Activity> API — form state, scroll position, uncommitted inputs all survive navigating away and coming back.

<RouteView nodeName="">
  <RouteView.Match segment="editor" keepAlive>
    <DocumentEditor />  {/* state preserved while navigated away */}
  </RouteView.Match>
  <RouteView.Match segment="preview">
    <PreviewPage />
  </RouteView.Match>
</RouteView>

Combines with fallback for lazy-loaded routes — <Activity> wraps the <Suspense> boundary.

Memoized <Link>

Link uses React.memo() with a custom areLinkPropsEqual comparator:

  • Primitive props (routeName, className, etc.) compared via Object.is.
  • routeParams / routeOptions compared via shallowEqual (Object.is per key, key-order insensitive).
  • 99% of Links pass routeParams=undefined and hit the Object.is(undefined, undefined) fast path.

Nested objects in params are not deep-compared — stabilize via useMemo if needed.

Hook caching via @real-router/sources

useRouteNode, useIsActiveRoute, useRouterTransition, RouterErrorBoundary all use cached factories from @real-router/sources. N consumers of the same nodeName share one router subscription, not N. Cache is per-router WeakMap — auto-evicts on router GC.


useSyncExternalStore — Native Foundation

React hooks read route state through useSyncExternalStore:

// Simplified internal pattern — useRouteNode
const store = useMemo(() => createRouteNodeSource(router, name), [router, name]);
const snapshot = useSyncExternalStore(
  store.subscribe,
  store.getSnapshot,
  store.getSnapshot, // SSR fallback — same snapshot on server and client
);

The third argument (getServerSnapshot) is provided because the router returns the same state on server and client — no hydration mismatch.


RouteView: Element Type Markers

RouteView.Match and RouteView.NotFound are real React components, but RouteView identifies them by element type (not by rendering them). That's why passing RouteView.Match outside RouteView does nothing — it's a marker, not a renderable component.

// CORRECT — used inside RouteView
<RouteView nodeName="">
  <RouteView.Match segment="users">
    <UsersPage />
  </RouteView.Match>
</RouteView>

// WRONG — Match is a marker, renders nothing on its own
const el = <RouteView.Match segment="users"><UsersPage /></RouteView.Match>;

Unlike Preact and Solid (which use Symbol $$type markers), React RouteView walks children via Children.toArray() and matches element.type === RouteView.Match.


Build: tsdown, Multi-Entry

Multi-entry config produces shared chunks for code used across entries:

dist/
├── esm/
│   ├── index.mjs           # Main (React 19.2+)
│   ├── legacy.mjs          # Legacy (React 18+)
│   ├── ink.mjs             # Ink (terminal)
│   └── chunk-*.mjs         # Shared hooks / utilities
└── cjs/
    └── (same layout)

Tree-shakeable — importing from /legacy doesn't pull the <Activity>-based RouteView.


Complete API Reference

Provider

Export Description
RouterProvider Root provider — pass router prop. Supports announceNavigation, scrollRestoration, viewTransitions.

RouterProvider accepts an optional announceNavigation boolean prop. When true, a visually hidden aria-live="assertive" region announces each route change and focus moves to the first <h1> on the new page. See Accessibility for full details.

<RouterProvider router={router} announceNavigation>
  <App />
</RouterProvider>

RouterProvider also accepts an optional scrollRestoration prop. When set to an options object, scroll position is captured on navigation and restored on back/forward. The React adapter watches primitive fields (mode, anchorScrolling), so inline object literals with the same values do not re-create the utility. See Scroll Restoration.

<RouterProvider router={router} scrollRestoration={{ mode: "restore" }}>
  <App />
</RouterProvider>

RouterProvider also accepts an optional viewTransitions boolean prop. When true, the browser's View Transitions API is wired into the router pipeline — subscribeLeave opens the VT snapshot of the old DOM and the router awaits the Promise; subscribe resolves the VT deferred via setTimeout(0). URL and UI stay in lock-step. No-op on SSR and browsers without document.startViewTransition. See View Transitions.

<RouterProvider router={router} viewTransitions>
  <App />
</RouterProvider>

Hooks

Export Returns Description
useRouter() Router Router instance (stable)
useNavigator() Navigator Navigator for navigate(), getState() (stable)
useRoute() { navigator, route, previousRoute } Current route + previousRoute (reactive)
useRouteNode(name) { navigator, route, previousRoute } Route state filtered by node (reactive)
useRouteUtils() RouteUtils Route tree utilities (stable)
useRouterTransition() { isTransitioning, toRoute, fromRoute } Transition state (reactive)
useRouteExit(handler, opts?) void Wrap subscribeLeave with abort + same-route guards; handler can return Promise
useRouteEnter(handler, opts?) void Fire on nav-driven mount with { route, previousRoute }; skip-initial built in

Components

Export Description
Link Navigation link with memo + active state
RouteView Declarative route matching (main entry only, supports keepAlive)
RouterErrorBoundary Declarative navigation error handling
InkRouterProvider / InkLink (/ink entry) Terminal UI primitives — see Ink Integration

Contexts

Export Description
RouterContext Raw Router — for advanced use
NavigatorContext Navigator — for advanced use
RouteContext { navigator, route, previousRoute } — for advanced use

Types

Export Description
RouteState { route, previousRoute }
LinkProps Props for <Link>
RouteViewProps Props for <RouteView>
MatchProps Props for <RouteView.Match> (includes keepAlive, fallback)
Navigator Re-exported from @real-router/core
RouterTransitionSnapshot Re-exported from @real-router/sources

Patterns

Reading Route Parameters

function UserProfile() {
  const { route } = useRouteNode("users.profile");
  return <h1>User: {route?.params.id}</h1>;
}

Typed Route Parameters

useRoute<P>() and useRouteNode<P>() accept an optional generic to type route.params without as casts at the call site.

type SearchParams = { q: string; sort: string } & Params;

function SearchPage() {
  const { route } = useRoute<SearchParams>();
  return <div>Query: {route.params.q}</div>;
}

useRoute() is non-nullable: route is guaranteed defined or the hook throws (issue #535). useRouteNode() keeps the nullable shape because node inactivity is a legitimate value, so route?.params.id above is correct for that hook.

Conditional Rendering

function ProtectedRoute() {
  const { route } = useRoute();
  if (!route) return <div>Loading...</div>;
  return <div>Page: {route.name}</div>;
}

Imperative Navigation

function BackButton() {
  const navigator = useNavigator();
  return <button onClick={() => navigator.navigate("home")}>Back</button>;
}

Transition Progress Bar

function ProgressBar() {
  const { isTransitioning, isLeaveApproved } = useRouterTransition();
  if (!isTransitioning) return null;
  return (
    <div
      className={isLeaveApproved ? "progress activating" : "progress leaving"}
      aria-live="polite"
    >
      Navigating...
    </div>
  );
}

Selective Re-Rendering via useRouteNode

Render only when the matched node changes, not on every navigation:

function UsersHeader() {
  // Re-renders only on users.* transitions, not on e.g. "home" → "about"
  const { route } = useRouteNode("users");
  return route ? <h2>Users section</h2> : null;
}

Stabilizing Params for <Link>

Link is memoized, but nested params objects change reference on every parent render. Stabilize via useMemo if needed:

function UserLink({ id }: { id: string }) {
  const params = useMemo(() => ({ id }), [id]);
  return <Link routeName="users.profile" routeParams={params}>View</Link>;
}

For primitive-only params ({ id: "123" }), inline is fine — shallowEqual handles it at ~40ns.


Lazy Loading with Suspense

RouteView.Match accepts a fallback prop. When provided, the matched content is wrapped in <Suspense> with that fallback.

import { lazy } from "react";

const LazyDashboard = lazy(() => import("./Dashboard"));

<RouteView nodeName="">
  <RouteView.Match segment="dashboard" fallback={<Spinner />}>
    <LazyDashboard />
  </RouteView.Match>
</RouteView>

Combines with keepAlive<Activity> wraps the whole match, including the <Suspense> boundary. State survives across navigations.


RouterErrorBoundary

Declarative handling of navigation errors — rejected guards, missing routes, thrown handlers:

import { RouterErrorBoundary } from "@real-router/react";

<RouterProvider router={router}>
  <RouterErrorBoundary
    fallback={(error, resetError) => (
      <div role="alert">
        <h2>Navigation error: {error.code}</h2>
        <button onClick={resetError}>Dismiss</button>
      </div>
    )}
  >
    <App />
  </RouterErrorBoundary>
</RouterProvider>

The fallback renders alongside children — links remain interactive while the error is showing. The error clears automatically on the next successful navigation.

Use onError for logging or analytics without affecting the UI:

<RouterErrorBoundary
  fallback={(error, resetError) => (
    <div className="toast">
      {error.code} <button onClick={resetError}>Dismiss</button>
    </div>
  )}
  onError={(error, toRoute, fromRoute) => {
    analytics.track("navigation_error", { code: error.code });
  }}
>
  <AppNavigation />
</RouterErrorBoundary>

Multiple boundaries in the tree see the same error — scope is global (the router dispatches errors to all subscribers). Per-Link scoping is not supported.

See RouterErrorBoundary for full API reference.


SSR

No built-in SSR support. For SSR:

  • Create router per request (don't share across requests).
  • Initialize with the matched URL (router.start(url)).
  • previousRoute will be undefined on server.

See the examples/react/ssr directory for a full Express + React 19 setup.


Performance

  • useRouteNode uses cached createRouteNodeSource from @real-router/sources — one subscription per (router, nodeName) shared across all consumers.
  • useRouterTransition uses getTransitionSource — shared eager source per router.
  • RouterErrorBoundary uses createDismissableError — shared error source with integrated dismissal state.
  • useIsActiveRoute uses cached createActiveRouteSource — params hashed via canonicalJson (key-order insensitive).
  • Link uses memo() with custom areLinkPropsEqual — primitive compare + shallowEqual for routeParams/routeOptions.
  • All WeakMap caches live in @real-router/sources — auto-evicted on router GC.

See Also

Navigation

Home


Concepts


Getting Started


Router Methods

Lifecycle

Navigation

State

URL & Path

Events


Standalone API

Tree-shakeable functions — import only what you need.

Routes — getRoutesApi(router)

Dependencies — getDependenciesApi(router)

Guards — getLifecycleApi(router)

Plugin Infrastructure — getPluginApi(router)

For plugin authors, not for general use.

SSR / SSG


React / Preact / Solid / Vue / Svelte Integration

Provider

Hooks

Components

SSR Components & Hooks

SSR-feature subpath — @real-router/{adapter}/ssr. Symmetric across React/Preact/Solid/Vue/Svelte.

  • Lazy (Svelte only — dynamic component import)
  • Await — read deferred SSR data by key
  • Streamed — Suspense-style boundary
  • ClientOnly — server fallback → client children swap
  • ServerOnly — server children, removed after hydration
  • HttpStatusCode — render-time HTTP status declaration
  • HttpStatusProvider — provides sink to descendant <HttpStatusCode>
  • useDeferred — read deferred Promise by key

DOM Utilities

Patterns


Subscription Layer (@real-router/sources)


Reactive Streams (@real-router/rx)


Plugins

Browser Plugin

Navigation Plugin

Hash Plugin

Memory Plugin

Lifecycle Plugin

Preload Plugin

Logger Plugin

Persistent Params

SSR Data

RSC Server

Validation

Search Schema

Utilities


Reference

Types

Error Codes

Clone this wiki locally