mrkeyoor.com_
Sat 19 Sept 15:50 UTC
npmWeb Frontendupdated 19 Sept 2026

react-router review

Our clean install of React Router 8.3.0 was quick, but a full browser import was substantial at 197.1 KB minified and 63.8 KB gzipped. The package handles URL matching, nested layouts, navigation, route loaders, actions, pending states, and error boundaries for React applications. You can use those pieces inside your own build in Data or Declarative mode, or adopt Framework mode for route modules, server rendering, prerendering, and middleware. Version 8 requires Node 22.22.0 and React 19.2.7, removes the react-router-dom compatibility package, and makes the former v8 future-flag behavior standard. Release 8.3.0 changes generated path parameters to follow RFC 3986 path-segment encoding, fixes NavLink pending state for destinations with a trailing slash, and updates custom entry requirements for the still-unstable RSC mode.

47.9Mdownloads / wk
Verdict

React Router 8.3.0 is a good fit when route loaders, actions, nested errors, and server rendering earn their place in the app. For a small client-only router, the 63.8 KB gzipped full-import result and version 8 runtime minimums make Wouter or a narrower setup easier to justify.

We installed it

Lab card: what happened when we installed react-routerScreenshot of react-router documentation
Install✓ · 1s3 packages on disk · 4 MB
ImportESM import works · require() works · ESM package with exports map
Browser63.8 KBgzipped (197.1 KB minified), bundled with esbuild
Typesno TypeScript types found
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does react-router install cleanly?

Yes. In a fresh container with an empty cache, npm install react-router finished in 1 seconds, leaving 3 packages and 4 MB on disk. npm audit reported no known vulnerabilities.

How much does react-router add to a browser bundle?

63.8 KB gzipped (197.1 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does react-router work with both ESM and CommonJS?

Yes. Both import 'react-router' and require('react-router') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does react-router include TypeScript types?

No type declarations were found in our install, so TypeScript users need their own declarations.

react-router or @tanstack/react-router: which should you use?

@tanstack/react-router: Choose it when compile-time route, parameter, and search-schema checks are central to the application. React Router 8.3.0 is a good fit when route loaders, actions, nested errors, and server rendering earn their place in the app.

When should you not use react-router?

Your deployment is below Node 22.22.0 or your app is below React and React DOM 19.2.7; version 8 declares those minimums, and Framework mode also requires Vite 7

API stability4/5The core route-object, loader, action, Outlet, Link, and navigation APIs carry forward into version 8, and the project published its breaking behavior behind v7 future flags first. The major still requires deliberate migration: react-router-dom is gone, middleware context is always RouterContextProvider, several future flags became defaults, raw request URL behavior changed, and the supported Node, React, and Vite floors all moved upward.
Docs5/5reactrouter.com separates Declarative, Data, and Framework modes instead of mixing their setup instructions. Its v7 upgrade guide names every version 8 minimum and gives import diffs for the removed react-router-dom package. The repository keeps release notes in one searchable changelog, and the guides cover loaders, actions, middleware, pending UI, errors, testing, and deployment. Readers still need to notice mode labels because the same concept can have a different API shape in Framework and Data mode.
Maintenance5/5The unarchived repository was pushed on August 21, 2026, one day before this measurement, and GitHub's issue search counted 99 open issues rather than mixing pull requests into that number. Version 8.3.0 shipped on July 22 with fixes for path encoding, trailing-slash NavLink state, session identifiers, and RSC request handling. The project also documents a yearly major-release schedule, so active maintenance comes with a predictable need to review migrations.
Ecosystem5/5npm recorded 51,783,781 downloads for the week ending August 22, 2026, and GitHub reported 56,571 stars. The repository publishes official packages for Node, Express, Cloudflare, Architect, file-system routes, development tooling, and a production server. That breadth helps when one route model must cross browser and server code, although framework users install more than the single react-router package measured in our sandbox.

Discussed on

  1. hnReact Router v4 FAQ159 points
  2. hnReact Router v5156 points
  3. hnReact Router 2.0.0119 points
  4. hnMerging Remix and React Router107 points
  5. hnReact Router: Declarative Routing for React87 points

Use it if

  • Your React app needs nested layouts, URL parameters, route-level errors, and pending navigation state under one routing model
  • You want loaders and actions tied to route matches, with automatic loader revalidation after a Form or fetcher mutation
  • You need the same route tree to support browser navigation and server rendering through an official framework or server adapter
  • Your team is moving a v7 app that already adopted the v8 future flags and can meet the new Node and React minimums
Skip it if

Setup reality

We installed react-router 8.3.0 in a fresh Node 22 container in 1 second. Three packages occupied 4 MB afterward, and npm audit reported 0 known vulnerabilities at every severity. The package has 1 direct dependency and 2 peer dependencies, with 3,648 KB unpacked. It declares ESM and has an exports map; both require() and ESM import worked in our check. We found no TypeScript type files. A full esbuild browser import measured 197.1 KB minified and 63.8 KB gzipped.

Library mode needs no credentials or config file. Install react-router beside React and React DOM 19.2.7 or newer, create the router once outside the React tree, then render RouterProvider from react-router/dom. Links, loaders, actions, and hooks come from react-router. Old react-router-dom imports fail on v8 because that compatibility package was removed. Choose a narrow import surface and let tree shaking work; our bundle figure imported the whole package namespace.

Framework mode changes the project shape. The official starter adds @react-router/dev, a Vite plugin, route modules, routes.ts, and react-router.config.ts. Vite 7 is the minimum. Loaders run before their matched route components render, while actions invoked by Form or fetcher cause loader data on the page to revalidate. That is convenient for server-backed screens, but it can repeat expensive reads unless your data layer caches or deduplicates them.

Middleware is always enabled in v8. A custom server's getLoadContext must return RouterContextProvider rather than the plain object accepted by older code. Raw request URLs can contain React Router's .data suffix and internal query details, so use the separate normalized url argument for routing decisions. RSC support remains unstable. Default RSC entries absorb the 8.3.0 changes, but custom entries may need client-version, subresource-integrity, and CSP nonce plumbing before they build and render correctly.

Patterns

Mount a data router create-browser-router

import { createBrowserRouter } from 'react-router';
import { RouterProvider } from 'react-router/dom';
import { createRoot } from 'react-dom/client';

const router = createBrowserRouter([
  { path: '/', Component: Home },
]);

createRoot(document.getElementById('root')).render(
  <RouterProvider router={router} />
);

Create the router outside the React tree. In v8, RouterProvider comes from react-router/dom rather than react-router-dom.

Render children through a layout render-nested-route

import { Outlet } from 'react-router';

function DashboardLayout() {
  return (
    <section>
      <DashboardNav />
      <Outlet />
    </section>
  );
}

const routes = [{
  path: '/dashboard',
  Component: DashboardLayout,
  children: [
    { index: true, Component: DashboardHome },
    { path: 'settings', Component: Settings },
  ],
}];

A matched child has nowhere to render if its parent omits Outlet. Index routes cannot have children.

Read a dynamic segment read-route-params

import { useParams } from 'react-router';

function Invoice() {
  const { invoiceId } = useParams();
  if (!invoiceId) return <p>Missing invoice id</p>;
  return <InvoiceDetails id={invoiceId} />;
}

const route = {
  path: '/invoices/:invoiceId',
  Component: Invoice,
};

URL parameters arrive as strings and may be absent. Parse and validate them before database or numeric use.

Show the active navigation item mark-active-link

import { NavLink } from 'react-router';

<NavLink
  to="/account"
  end
  className={({ isActive, isPending }) =>
    isPending ? 'pending' : isActive ? 'active' : undefined
  }
>
  Account
</NavLink>

The end prop prevents /account from staying active on every descendant. Version 8.3.0 fixes pending state when to ends in a slash.

Fetch data before rendering load-route-data

import { useLoaderData } from 'react-router';

const route = {
  path: '/teams/:teamId',
  loader: async ({ params }) => ({
    team: await getTeam(params.teamId),
  }),
  Component: Team,
};

function Team() {
  const { team } = useLoaderData();
  return <h1>{team.name}</h1>;
}

The loader runs before this matched component renders and runs again when the router revalidates the route.

Post a form to an action submit-route-action

import { Form, redirect } from 'react-router';

const route = {
  path: '/projects/new',
  action: async ({ request }) => {
    const values = await request.formData();
    const project = await createProject(values);
    return redirect(`/projects/${project.id}`);
  },
  Component: NewProject,
};

function NewProject() {
  return <Form method="post"><input name="title" /><button>Create</button></Form>;
}

A Form submission navigates and revalidates loader data after the action. Validate form values inside the action.

Save through a fetcher mutate-without-navigation

import { useFetcher } from 'react-router';

function RenameTask({ id, title }) {
  const fetcher = useFetcher();
  return (
    <fetcher.Form method="post" action={`/tasks/${id}`}>
      <input name="title" defaultValue={title} />
      <button disabled={fetcher.state !== 'idle'}>
        {fetcher.state === 'idle' ? 'Save' : 'Saving'}
      </button>
    </fetcher.Form>
  );
}

A fetcher calls the route action without adding a history entry. Its state is separate from global navigation state.

Render pending navigation UI show-navigation-progress

import { useNavigation } from 'react-router';

function GlobalProgress() {
  const navigation = useNavigation();
  if (navigation.state === 'idle') return null;
  return (
    <p role="status">
      {navigation.state === 'submitting' ? 'Submitting' : 'Loading'}
    </p>
  );
}

useNavigation tracks navigations and Form submissions. Fetcher work has its own state and does not appear here.

Keep a filter in the URL update-search-params

import { useSearchParams } from 'react-router';

function SearchFilter() {
  const [params, setParams] = useSearchParams();
  return (
    <input
      value={params.get('q') ?? ''}
      onChange={(event) => {
        const next = new URLSearchParams(params);
        next.set('q', event.target.value);
        setParams(next, { replace: true });
      }}
    />
  );
}

Replacing history prevents each keystroke from becoming a Back-button stop. URLSearchParams values still need parsing and validation.

Catch errors near the failing route handle-route-error

import { data, isRouteErrorResponse, useRouteError } from 'react-router';

async function loader({ params }) {
  const invoice = await getInvoice(params.invoiceId);
  if (!invoice) throw data('Invoice not found', { status: 404 });
  return invoice;
}

function InvoiceError() {
  const error = useRouteError();
  return isRouteErrorResponse(error)
    ? <h1>{error.status}: {error.data}</h1>
    : <h1>Invoice failed to load</h1>;
}

Assign InvoiceError as the route's ErrorBoundary. Errors bubble to the closest ancestor boundary when a route has none.

Redirect from route middleware protect-with-middleware

import { redirect } from 'react-router';

async function requireUser({ request }) {
  const user = await readUser(request);
  if (!user) throw redirect('/login');
}

const routes = [{
  path: '/dashboard',
  middleware: [requireUser],
  loader: loadDashboard,
  Component: Dashboard,
}];

Middleware is always enabled in v8. Framework server middleware and browser Data-mode middleware do not run in identical environments.

Load a route module on demand lazy-load-route

import { createBrowserRouter } from 'react-router';

const router = createBrowserRouter([{
  path: '/settings',
  lazy: async () => {
    const module = await import('./settings-route.js');
    return {
      Component: module.Settings,
      loader: module.loader,
      ErrorBoundary: module.SettingsError,
    };
  },
}]);

The lazy function can supply route implementation fields, but it cannot change matching fields such as path, index, or children.

Alternatives

PackageRegistryPick it when
@tanstack/react-routernpmChoose it when compile-time route, parameter, and search-schema checks are central to the application
wouternpmChoose it for a small client-only React app that needs matching and links without route loaders or framework features
nextnpmChoose it when you want file routing, React Server Components, rendering, and deployment conventions supplied as one framework

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.