npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@real-router/react

v0.31.47

Published

React integration for Real-Router

Readme

@real-router/react

npm npm downloads bundle size License: MIT

React integration for Real-Router — hooks, components, and context providers.

Installation

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

Peer dependency: main entry requires react >= 19.2.0 (uses <Activity>); @real-router/react/legacy works with react >= 18.0.0; @real-router/react/ink requires react >= 19.2 and ink >= 7.0.0 (Ink v7 itself pins React 19.2+).

Entry Points

| Import Path | React Version | Runtime | Includes | | ----------------------------------------------- | ------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | @real-router/react | 19.2+ | DOM | Full client API (hooks, Link, RouteView with keepAlive, RouterErrorBoundary). No SSR-feature components — those live at /ssr. | | @real-router/react/ssr | 19.2+ | DOM (SSR-aware) | <ClientOnly>, <ServerOnly>, <Await>, <Streamed>, useDeferred, <HttpStatusCode>, <HttpStatusProvider>, createHttpStatusSink | | @real-router/react/legacy | 18+ | DOM | Client API for React 18 (no RouteView, no SSR helpers) | | @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)) | | @real-router/react/ink | 19.2+ | Terminal (Ink 7+) | Hooks, InkRouterProvider, InkLink — no Link, no RouteView, no announceNavigation. ESM-only (import only): ink itself is ESM-only with top-level await, so a require-able build of this entry cannot exist | | @real-router/react (react-server condition) | 19+ | RSC bundler | Type-only re-exports for Server Components — no client runtime. Same condition applies to /ssr for prop types. |

All client entries share the same underlying hook code. /legacy excludes React 19.2 <Activity>; /ink excludes DOM-bound primitives (<a>-based Link, announceNavigation) and replaces them with keyboard-driven terminal equivalents. The /ssr split keeps server-only prop types out of the client TypeScript context for apps that don't touch SSR (bundle cost is ≈ 0 thanks to "sideEffects": false).

The root export resolves to a type-only entry when bundlers apply the react-server condition (Vite RSC, Webpack RSC, Turbopack, Parcel) — Server Components can import public API types without pulling client-only code into the server bundle. Per-request data fetching is handled by @real-router/rsc-server-plugin, not this entry. See RSC Integration wiki guide.

Quick Start

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

const router = createRouter([
  { name: "home", path: "/" },
  {
    name: "users",
    path: "/users",
    children: [{ 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">
          <UsersPage />
        </RouteView.Match>
        <RouteView.NotFound>
          <NotFoundPage />
        </RouteView.NotFound>
      </RouteView>
    </RouterProvider>
  );
}

Hooks

| Hook | Returns | Re-renders | | ---------------------------------- | --------------------------------------------------------------------- | --------------------------------------- | | useRouter() | Router | Never | | useNavigator() | Navigator | Never (stable ref, safe to destructure) | | useRoute() | { navigator, route, previousRoute } | Every navigation | | useRouteNode(name) | { navigator, route, previousRoute } | Only when node activates/deactivates | | useRouteUtils() | RouteUtils | Never | | useRouterTransition() | { isTransitioning, isLeaveApproved, toRoute, fromRoute } | On transition start/end | | useRouteExit(handler, options?) | void — wraps router.subscribeLeave with abort + same-route guards | Never (stable subscription) | | useRouteEnter(handler, options?) | void — fires on nav-driven mount via useRoute() snapshot | Never (handler stays current) |

// useRouteNode — re-renders only when "users.*" changes
function UsersLayout() {
  const { route } = useRouteNode("users");
  if (!route) return null;

  switch (route.name) {
    case "users":
      return <UsersList />;
    case "users.profile":
      return <UserProfile id={route.params.id} />;
    default:
      return null;
  }
}

// useNavigator — stable reference, never causes re-renders
function BackButton() {
  const navigator = useNavigator();
  return <button onClick={() => navigator.navigate("home")}>Back</button>;
}

// useRouterTransition — progress bars, loading states
function GlobalProgress() {
  const { isTransitioning } = useRouterTransition();
  if (!isTransitioning) return null;
  return <div className="progress-bar" />;
}

// useRouteExit — exit animations, draft autosave, AbortSignal-aware cleanup
function FormPage() {
  useRouteExit(async ({ signal }) => {
    await api.saveDraft(formState, { signal });
  });
  return <Form />;
}

// useRouteEnter — page-enter analytics, focus management, entry animations
function AboutPage() {
  useRouteEnter(({ route, previousRoute }) => {
    analytics.track("page_enter", {
      route: route.name,
      from: previousRoute.name,
    });
  });
  return <About />;
}

Components

<Link>

Navigation link with automatic active state detection. Re-renders only when its active status changes.

<Link
  routeName="users.profile"
  routeParams={{ id: "123" }} // path channel (RFC-4 M2)
  routeSearch={{ tab: "posts" }} // query channel (RFC-4 M2)
  activeClassName="active" // default: "active"
  activeStrict={false} // default: false (ancestor match)
  ignoreQueryParams={true} // default: true
  routeOptions={{ replace: true }}
>
  View Profile
</Link>

hash prop — URL fragment / tab-style UIs

<nav>
  <Link routeName="settings" hash="profile">
    Profile
  </Link>
  <Link routeName="settings" hash="account">
    Account
  </Link>
  <Link routeName="settings" hash="billing">
    Billing
  </Link>
</nav>

Tri-state: undefined preserves the current hash, "" clears it, a value sets it. Active class is hash-aware — only the matching tab lights up. Live demo: examples/web/react/hash-examples/link-hash/. See the Hash Fragment Support wiki page for the full surface.

<RouteView> (React 19.2+)

Declarative route matching with optional keepAlive — preserves component state via React's <Activity> API.

<RouteView nodeName="">
  <RouteView.Match segment="users" keepAlive>
    <UsersPage /> {/* State preserved when navigating away */}
  </RouteView.Match>
  <RouteView.Match segment="settings">
    <SettingsPage /> {/* Unmounts normally */}
  </RouteView.Match>
  <RouteView.NotFound>
    <NotFoundPage />
  </RouteView.NotFound>
</RouteView>

RouteView.Match props

| Prop | Type | Description | | ----------- | ----------- | --------------------------------------------------------------------------- | | segment | string | Route segment to match | | exact | boolean | Exact match only — no descendants. Defaults to false. | | keepAlive | boolean | Preserve state via React <Activity> (React 19.2+) | | fallback | ReactNode | Shown while children suspend. Wraps children in <Suspense> when provided. |

Lazy loading with fallback

Pass fallback to code-split a route component. RouteView.Match wraps children in <Suspense> automatically:

import { lazy } from "react";

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

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

fallback and keepAlive work together — <Activity> wraps the whole match including the <Suspense> boundary.

RouteView.Self

Renders when the active route name exactly equals the parent <RouteView>'s nodeName. Use it for leaf views where the parent route itself is the destination — e.g. /users rendering a directory page while /users/:id renders inside a nested <RouteView nodeName="users">.

<RouteView nodeName="users">
  <RouteView.Self>
    <UsersIndex /> {/* rendered for route name === "users" */}
  </RouteView.Self>
  <RouteView.Match segment="profile">
    <UserProfile /> {/* rendered for "users.profile" and descendants */}
  </RouteView.Match>
  <RouteView.NotFound>
    <NotFoundPage />
  </RouteView.NotFound>
</RouteView>

| Prop | Type | Description | | ---------- | ----------- | ------------------------------------------------------------------------------------------ | | fallback | ReactNode | Symmetric with RouteView.Match — wraps children in <Suspense> when defined. | | children | ReactNode | Content to render when the active route name equals the parent <RouteView>'s nodeName. |

First-wins: if multiple <RouteView.Self> elements appear, only the first contributes to the rendered output (same precedence semantics as <RouteView.NotFound>). An activating <RouteView.Match> suppresses both Self and NotFound.

<RouterErrorBoundary>

Declarative error handling for navigation errors. Shows a fallback alongside children (not instead of) when a guard rejects or a route is not found.

<RouterErrorBoundary
  fallback={(error, resetError) => (
    <div className="toast">
      {error.code} <button onClick={resetError}>Dismiss</button>
    </div>
  )}
  onError={(error) => analytics.track("nav_error", { code: error.code })}
>
  <Link routeName="protected">Go to Protected</Link>
</RouterErrorBoundary>

Auto-resets on next successful navigation. Works with both <Link> and imperative router.navigate().

Available from both @real-router/react and @real-router/react/legacy.

<ClientOnly> / <ServerOnly>

Paired SSR-aware boundaries. <ClientOnly> renders fallback on the server (and on the client first paint, to match SSR HTML), then swaps in children after mount. <ServerOnly> is the symmetric inverse.

import { ClientOnly, ServerOnly } from "@real-router/react/ssr";

<ClientOnly fallback={<Skeleton />}>
  <BrowserApiWidget />
</ClientOnly>

<ServerOnly>
  <SeoMetaStrip />
</ServerOnly>

Implementation: useState(false) + useEffect(() => setMounted(true), []). Server emits the SSR-side branch, client first paint matches it (no hydration mismatch), the post-mount effect triggers a single re-render that swaps the rendered branch.

Available from @real-router/react/ssr and @real-router/react/legacy/ssr. End-to-end dogfooding lives in examples/web/react/ssr-examples/ssr/ (see e2e/ssr-boundaries.spec.ts).

<Streamed> / <Await> / useDeferred

Three pieces of the deferred-data pipeline (paired with @real-router/ssr-data-plugin's defer() API). <Streamed> is a cross-adapter alias for <Suspense> so route bundles can use the same boundary name across Solid/Vue/Svelte/React. <Await<T> name="key"> reads the deferred promise the loader published under that key and hands the resolved value to a render-prop. useDeferred<T>(key) returns the same promise for callers that want to compose with use() or a third-party suspense library.

import { Streamed, Await, useDeferred } from "@real-router/react/ssr";

// Render-prop form — works in React 19.2+ via internal `use(promise)`.
<Streamed fallback={<Spinner />}>
  <Await<Review[]> name="reviews">
    {(reviews) => <ReviewList items={reviews} />}
  </Await>
</Streamed>;

// Manual form — works on React 18+ (`/legacy/ssr` entry).
function Reviews() {
  const reviews = use(useDeferred<Review[]>("reviews"));
  return <ReviewList items={reviews} />;
}

<Await> is React 19.2+ only (depends on use(promise)); <Streamed> and useDeferred ship in both /ssr and /legacy/ssr. End-to-end example: examples/web/react/ssr-examples/ssr-streaming/.

<HttpStatusCode> / <HttpStatusProvider> / createHttpStatusSink

Render-time HTTP status declaration for SSR responses. Mount <HttpStatusCode code={N} /> inside a route component (typical use: a <RouteView.NotFound> glob page) — it writes N to the nearest <HttpStatusProvider>'s sink during render and returns null. After renderToString / renderToReadableStream, read sink.code and pass it to your response.

// app.tsx
import { HttpStatusCode } from "@real-router/react/ssr";

function NotFound() {
  return (
    <>
      <HttpStatusCode code={404} />
      <h1>Page not found</h1>
    </>
  );
}

// entry-server.tsx
import { renderToString } from "react-dom/server";
import {
  HttpStatusProvider,
  createHttpStatusSink,
} from "@real-router/react/ssr";

const sink = createHttpStatusSink();
const html = renderToString(
  <HttpStatusProvider sink={sink}>
    <RouterProvider router={router}>
      <App />
    </RouterProvider>
  </HttpStatusProvider>,
);
response.status(sink.code ?? 200).send(html);

| Export | Kind | Purpose | | ------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | <HttpStatusCode code={N}/> | component | Writes code to the optional context sink during render. Last write wins across multiple instances. No-op without a provider. | | <HttpStatusProvider sink={…}> | component | Supplies an HttpStatusSink to descendant <HttpStatusCode /> via React context. | | createHttpStatusSink() | utility | Returns a fresh { code: number \| undefined } sink — construct one per request on the server, read sink.code after rendering. |

Loader-driven errors (LoaderNotFound → 404, LoaderRedirect → 30x) keep working as before; this component covers render-time decisions only. Streaming SSR caveat: mount <HttpStatusCode> in the shell (above every <Suspense> that could delay it), or await stream.allReady before reading sink.code — once the response status flushes, later writes are lost.

Available from @real-router/react/ssr and @real-router/react/legacy/ssr.

React 18 Migration

One import path change — all hooks and Link work identically:

- import { useRouteNode, Link } from '@real-router/react';
+ import { useRouteNode, Link } from '@real-router/react/legacy';

RouteView is not available from /legacy. Use useRouteNode with a switch/case pattern instead.

useRouteExit and useRouteEnter are also not available from /legacy — they depend on React 19's concurrent-mode scheduling guarantees. Use router.subscribeLeave() directly for exit guards, and useEffect for mount-time analytics on React 18.

Ink (Terminal UI)

@real-router/react/ink lets you build terminal apps with the same hooks you use in the browser.

The official Ink routing recipe (vadimdemedes/ink#874, merged Feb 2026) recommends React Router's MemoryRouter plus hand-rolled useInput / useNavigate per menu item — there's no Link-equivalent because RR's <Link> renders HTML anchors, which terminals can't handle. We ship that packaged: <InkLink> is focus-aware out of the box (joins Ink's focus ring, Enter navigates, activeColor/focusColor props), and @real-router/memory-plugin replaces MemoryRouter. No boilerplate per menu entry.

Ships three entry-specific pieces alongside the shared hooks:

  • InkRouterProvider — drop-in provider, no DOM, no aria-live.
  • InkLink — focusable text link. Joins Ink's focus ring via useFocus; Enter navigates.
  • Hooks re-exported unchanged.
import { createRouter } from "@real-router/core";
import { memoryPluginFactory } from "@real-router/memory-plugin";
import {
  InkLink,
  InkRouterProvider,
  useRouteNode,
} from "@real-router/react/ink";
import { Box, Text, render } from "ink";

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

router.usePlugin(memoryPluginFactory());
await router.start("/");

const App = () => {
  const { route } = useRouteNode("");

  return (
    <Box flexDirection="column" paddingX={1}>
      <Box columnGap={2}>
        <InkLink
          routeName="home"
          focusColor="cyan"
          activeColor="green"
          autoFocus
        >
          [ Home ]
        </InkLink>
        <InkLink routeName="users" focusColor="cyan" activeColor="green">
          [ Users ]
        </InkLink>
      </Box>
      <Text>Current: {route?.name}</Text>
    </Box>
  );
};

render(
  <InkRouterProvider router={router}>
    <App />
  </InkRouterProvider>,
);

Navigation contract: Tab moves focus between InkLinks, Enter calls router.navigate(...). RouteView and the DOM Link are intentionally absent from this entry — compose routes with useRouteNode("") and a switch. RouterErrorBoundary is available for terminal error handling.

Install:

npm install @real-router/react @real-router/core @real-router/memory-plugin ink

ink is an optional peer dependency — only install it if you use /ink.

Migration from react-router5

| API | react-router5 | @real-router/react | | ------------------------------------------------------ | ------------- | ------------------ | | RouterProvider, Link | Yes | Yes | | useRouter, useRoute, useRouteNode | Yes | Yes | | RouteView with keepAlive | No | Yes (React 19.2+) | | useNavigator, useRouteUtils, useRouterTransition | No | Yes | | RouterErrorBoundary (declarative error handling) | No | Yes | | withRouter, withRoute, routeNode (HOCs) | Yes | No — use hooks | | Router, Route, RouteNode (render props) | Yes | No — use hooks |

Accessibility

Enable screen reader announcements for route changes:

<RouterProvider router={router} announceNavigation>
  {/* Your app */}
</RouterProvider>

When enabled, a visually hidden aria-live region announces each navigation. Focus moves to the first <h1> on the new page. See Accessibility guide for details.

announceNavigation also accepts a RouteAnnouncerOptions object to customize the announced text:

| Option | Type | Description | | --------------------- | ------------------- | --------------------------------------------------------------------------------------------------- | | prefix | string | Prefix prepended to the resolved text (default "Navigated to ") | | getAnnouncementText | (route) => string | Full custom text; overrides the default h1 → title → route-name chain (falls back on empty/throw) |

<RouterProvider
  router={router}
  announceNavigation={{
    getAnnouncementText: (route) => `Now on ${route.name}`,
  }}
>
  {/* Your app */}
</RouterProvider>

Scroll Restoration

Opt-in preservation of scroll position across navigations:

<RouterProvider router={router} scrollRestoration={{ mode: "restore" }}>
  {/* Your app */}
</RouterProvider>

Restores scroll on back/forward, scrolls to top (or #hash) on push. Three modes: "restore" (default), "top", "native". Custom containers via scrollContainer: () => HTMLElement | null. Lifecycle tied to the provider — created on mount, destroyed on unmount. Under @real-router/browser-plugin, replace transitions now preserve scroll position and programmatic reloads restore from sessionStorage (portable via state.transition.replace / state.transition.reload). See Scroll Restoration guide for the full behaviour matrix.

Scroll Spy

Opt-in router-coordinated IntersectionObserver scroll spy — the URL hash tracks the topmost visible anchor as the user scrolls, syncing state.context.url.hash so sibling <Link hash> highlights stay current:

<RouterProvider router={router} scrollSpy={{ selector: "[id]:is(h2,h3)" }}>
  {/* Your app */}
</RouterProvider>

Emits a forced same-route transition with { hash, replace: true, force: true, hashChange: true } — the same write API as <Link hash> (#532), just with replace: true so the spy doesn't pollute history. Three anti-flicker gates: isTransitioning (skip emits during transitions), coolingDown (skip emits during smooth scrollIntoView after a <Link hash> click; cleared on scrollend or 500 ms timeout), and selfEmitting (the spy doesn't rate-limit itself). Hardcoded internals: IntersectionObserver.threshold = 0, rAF + 150 ms trailing debounce, MutationObserver re-observe debounced 250 ms.

Options: { selector: string, rootMargin?: string, scrollContainer?: () => HTMLElement | null }. Default rootMargin: "-20% 0px -60% 0px" (anchor active when crossing the top 20% of the viewport). Empty selector / undefined = off. SSR / browsers without IntersectionObserver = NOOP. Requires browser-plugin or navigation-plugin (hash-plugin and memory-plugin → warn-once + NOOP — they don't claim state.context.url).

Live demo: examples/web/react/hash-examples/scroll-spy/ (12 sections, TOC sidebar, plugin & spy-mode switchers, 10 e2e scenarios). See Scroll Spy guide for the full API surface and recipes.

View Transitions

Opt-in animated route transitions via the browser's View Transitions API:

<RouterProvider router={router} viewTransitions>
  {/* Your app */}
</RouterProvider>

No-op on unsupported browsers (Firefox as of 2026-04, SSR). Customization is pure CSS via ::view-transition-* pseudo-elements and view-transition-name for hero morphs. See View Transitions guide for patterns.

Documentation

Full documentation: Wiki

Examples

14 runnable examples — each is a standalone Vite app. Run: cd examples/web/react/basic && pnpm dev

basic · nested-routes · auth-guards · data-loading · lazy-loading · async-guards · hash-routing · persistent-params · error-handling · dynamic-routes · keep-alive · legacy-entry · hmr · combined

Related Packages

| Package | Description | | ---------------------------------------------------------------------------------------- | ------------------------------------ | | @real-router/core | Core router (required dependency) | | @real-router/browser-plugin | Browser History API integration | | @real-router/sources | Subscription layer (used internally) | | @real-router/route-utils | Route tree queries (useRouteUtils) |

Contributing

See contributing guidelines for development setup and PR process.

License

MIT © Oleg Ivanov