mrkeyoor.com_
Wed 05 Aug 05:03 UTC
npmWeb Frontendupdated 05 Aug 2026

react-router

React Router is a multi-strategy router for React that you can use two very different ways. Library mode keeps your own build and gives you nested routes, links, params, and navigation state. Framework mode (the Remix lineage) adds a Vite plugin with route modules, loaders, actions, middleware, SSR, and pre-rendering, making it a full-stack framework. v8 is ESM-only, sits on an open governance model, and majors now ship on a planned yearly cadence each June.

Verdict

The default router for React and, in framework mode, a credible full-stack framework with the Remix ideas built in. Adopt future flags as they appear and the yearly majors stay boring; ignore them and each June becomes a migration project.

API stability4/5The future-flag system genuinely de-risks majors for teams that adopt flags early, but the ecosystem still churns: react-router-dom was removed in v8 and v8 went ESM-only with a raised Node/React/Vite baseline
Docs5/5reactrouter.com has separate framework and library getting-started tracks, per-major upgrade guides, and a changelog maintained as one searchable file
Maintenance5/5Open governance with a planned yearly release cadence, active daily development, and the Remix team's full attention since the projects merged
Ecosystem5/5The assumed default in React tutorials and starters for a decade; official adapters for Node, Express, Cloudflare, and Architect ship from the same repo

Use it if

  • Your SPA needs nested layouts, URL params, and data loading tied to routes rather than scattered in components
  • You want a full-stack React framework with loaders, actions, middleware, and SSR while staying on the React Router API
  • You are on v7 with the future flags adopted; the v8 upgrade is deliberately boring by design
Skip it if

Setup reality

Library mode is one install and a router component. Framework mode is a different project shape: the @react-router/dev Vite plugin, a routes.ts file, generated route types, and entry files, so start from create-react-router instead of bolting it onto an existing Vite app. v8 removed the react-router-dom package entirely: RouterProvider and HydratedRouter now import from react-router/dom and everything else from react-router. Middleware is always enabled, and custom-server getLoadContext must return a RouterContextProvider, which breaks v7 servers that returned plain objects.

Patterns

Create a data router and mount itbrowser-router

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

const router = createBrowserRouter([
  {
    path: '/',
    Component: Root,
    children: [
      { index: true, Component: Home },
      { path: 'teams/:teamId', Component: Team },
    ],
  },
]);

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

In v8 RouterProvider imports from react-router/dom; the react-router-dom package no longer exists.

Read dynamic URL paramsroute-params

import { useParams } from 'react-router';

function Team() {
  const { teamId } = useParams();
  return <h1>Team {teamId}</h1>;
}

Params are always strings; parse numbers and validate ids yourself.

Render child routes inside a layoutnested-layout

import { Outlet } from 'react-router';

function Root() {
  return (
    <>
      <Nav />
      <main>
        <Outlet />
      </main>
    </>
  );
}

Without an Outlet, child routes match but render nothing, which is the classic blank-page bug.

Navigate from an event handlernavigate-programmatic

import { useNavigate } from 'react-router';

function Logout() {
  const navigate = useNavigate();
  return (
    <button onClick={async () => {
      await logout();
      navigate('/login', { replace: true });
    }}>Log out</button>
  );
}

For data-driven redirects, return redirect() from a loader or action instead of navigating in an effect.

Style the active navigation linkactive-link

import { NavLink } from 'react-router';

<NavLink
  to="/messages"
  className={({ isActive }) => (isActive ? 'active' : '')}
>
  Messages
</NavLink>

NavLink matches descendant paths too; add the end prop for exact matching on parent routes like '/'.

Load data before a route rendersroute-loader

import { useLoaderData } from 'react-router';

const router = createBrowserRouter([
  {
    path: 'teams/:teamId',
    loader: async ({ params }) => fetchTeam(params.teamId),
    Component: Team,
  },
]);

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

Loaders for all matched routes run in parallel before render; throw a Response from a loader to hit the error boundary.

Mutate data with a route action and Formform-action

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

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

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

Form submits to the route action without manual state; useNavigation().state drives pending UI and loaders revalidate after the action.

Read and write query string statesearch-params

import { useSearchParams } from 'react-router';

function List() {
  const [searchParams, setSearchParams] = useSearchParams();
  const page = Number(searchParams.get('page') ?? 1);
  return (
    <button onClick={() => {
      setSearchParams(prev => {
        prev.set('page', String(page + 1));
        return prev;
      });
    }}>Next page</button>
  );
}

Setting search params pushes a history entry by default; pass { replace: true } for filter UIs so back does not replay every keystroke.

Handle route errors in placeerror-boundary

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

const route = { path: '/', Component: Root, ErrorBoundary: RootError };

function RootError() {
  const error = useRouteError();
  if (isRouteErrorResponse(error)) {
    return <h1>{error.status} {error.statusText}</h1>;
  }
  return <h1>Something broke</h1>;
}

Errors bubble to the nearest ErrorBoundary up the route tree, so one on the root route catches everything as a fallback.

Redirect unauthenticated users from a loaderprotect-route

import { redirect } from 'react-router';

const route = {
  path: 'dashboard',
  loader: async () => {
    const user = await getUser();
    if (!user) throw redirect('/login');
    return user;
  },
  Component: Dashboard,
};

Throwing redirect from a loader runs before render, so protected content never flashes; in v8 middleware can share this across routes.

Code-split a route in library modelazy-route

const router = createBrowserRouter([
  {
    path: 'settings',
    lazy: async () => {
      const mod = await import('./settings');
      return { Component: mod.Settings, loader: mod.loader };
    },
  },
]);

Framework mode splits route modules automatically (splitRouteModules defaults on in v8); lazy is the manual equivalent for library mode.

Alternatives

PackageRegistryPick it when
@tanstack/react-routernpmYou want fully type-safe routes and search params checked at compile time
wouternpmA minimal hook-based router covers your small SPA and you do not need loaders or data APIs
nextnpmYou want routing, SSR, RSC, and bundling as one integrated framework rather than assembled parts