mrkeyoor.com_
Thu 06 Aug 02:45 UTC
npmWeb Frontendupdated 06 Aug 2026

react-router-dom

react-router-dom used to be the web build of React Router. Since v6.4 it is not that any more. Its entire published source is a re-export of the react-router package, and its own npm README says so in four lines: it exists to smooth the upgrade path for v6 applications, and you should change your imports and remove it from your dependencies. The routing itself lives in react-router, which in v7 covers three usage modes: declarative (BrowserRouter and Routes, the classic v5 and v6 shape), data (createBrowserRouter with loaders, actions and error boundaries), and framework (a Vite plugin with file routes, server rendering and typed route modules, which is the former Remix). react-router-dom is the compatibility door into that, and it is a door that is being bricked up.

Verdict

The router is excellent and the package is a leftover. Keep react-router-dom only while migrating an existing v6 app, and install react-router in anything new, because v8 deletes this package outright.

API stability2/5This specific package is frozen at 7.18.2 and removed in v8, and the library behind it has broken compatibility at v6, again at v6.4 with modes, and again at v7. v8 lands behind five future flags you are expected to adopt while still on v7
Docs4/5reactrouter.com is thorough, versioned and has real upgrade guides, but every page is tagged with which of the three modes it applies to, so you constantly check whether an example is even valid for your setup
Maintenance5/5Pushed to within the day, releases every few weeks, and around 97 open issues plus roughly 56 open PRs against 56.5k stars, which is a healthy ratio for a project this size. The maintenance is on the react-router package, not on this shim
Ecosystem5/543M weekly downloads, the default answer for React routing for a decade, and integrations in nearly every React UI kit, testing helper and meta-framework

Use it if

  • You are maintaining a v6 codebase with hundreds of files importing from react-router-dom and you want to move to v7 without touching every import in the same pull request
  • You depend on a third-party component library whose peerDependencies still name react-router-dom rather than react-router, so removing it breaks installs
  • You want data mode features (route loaders, actions, useNavigation pending states, route-level error boundaries) in an existing client-rendered React app without adopting a full framework
  • You need the widest possible pool of tutorials, Stack Overflow answers and AI-generated code to actually match your imports, since almost all of it still says react-router-dom
Skip it if

Setup reality

npm install react-router-dom pulls in react-router at an exact pinned version plus cookie and set-cookie-parser, and needs React 18 or newer as a peer dependency with Node 20+ for tooling. Installation is easy; the decisions are not. First you pick a mode, and the choice is not reversible for free: declarative mode gets you BrowserRouter and Routes in five minutes, data mode needs your whole route tree defined as an object or with createRoutesFromElements before any loader runs, and framework mode means a Vite plugin, a routes.ts file, generated types under .react-router/types, and a server. Second, if you deploy an SPA to static hosting you have to configure a catch-all rewrite to index.html or every deep link 404s. Third, TypeScript users in framework mode must run react-router typegen (or the dev server) before typecheck, or CI fails on missing generated types. And whichever mode you pick, plan the import rewrite from react-router-dom to react-router now rather than later.

Patterns

Classic declarative routingdeclarative-routes

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

export default function App() {
  return (
    <BrowserRouter>
      <nav><Link to="/about">About</Link></nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

In declarative mode a loader prop on Route is ignored with no warning, so components render with undefined data if you copy a data-mode example.

Data mode with createBrowserRouterdata-router-setup

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

const router = createBrowserRouter([
  {
    path: '/',
    Component: Root,
    errorElement: <RootError />,
    children: [
      { index: true, Component: Home },
      { path: 'posts/:postId', Component: Post, loader: postLoader },
    ],
  },
]);

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

Loaders, actions and fetchers only exist in data and framework mode; BrowserRouter cannot run them.

Nested layout with Outletnested-layout-outlet

import { Outlet, NavLink } from 'react-router-dom';

function Root() {
  return (
    <div className="shell">
      <aside>
        <NavLink to="/inbox" className={({ isActive }) => isActive ? 'on' : ''}>
          Inbox
        </NavLink>
      </aside>
      <main><Outlet /></main>
    </div>
  );
}

Forgetting Outlet in a parent route component is the most common reason child routes match but render nothing.

Load data before the route rendersroute-loader

import { useLoaderData } from 'react-router-dom';

export async function postLoader({ params, request }) {
  const res = await fetch(`/api/posts/${params.postId}`, {
    signal: request.signal,
  });
  if (!res.ok) throw new Response('Not found', { status: 404 });
  return { post: await res.json() };
}

function Post() {
  const { post } = useLoaderData();
  return <article>{post.title}</article>;
}

Return plain objects in v7; the json() helper is deprecated. Pass request.signal to fetch so navigating away cancels the request.

Mutate with Form and an actionform-action-mutation

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

export async function createPost({ request }) {
  const form = await request.formData();
  const res = await fetch('/api/posts', { method: 'POST', body: form });
  const post = await res.json();
  return redirect(`/posts/${post.id}`);
}

function NewPost() {
  const nav = useNavigation();
  const busy = nav.state === 'submitting';
  return (
    <Form method="post">
      <input name="title" />
      <button disabled={busy}>{busy ? 'Saving' : 'Save'}</button>
    </Form>
  );
}

After an action, every loader on the matched route tree refetches automatically; you do not invalidate a cache by hand.

Handle route errorsroute-error-boundary

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

function RootError() {
  const error = useRouteError();
  if (isRouteErrorResponse(error)) {
    return <h1>{error.status} {error.statusText}</h1>;
  }
  return <h1>{error instanceof Error ? error.message : 'Unknown error'}</h1>;
}

A thrown Response becomes a route error response; a thrown Error does not, so check with isRouteErrorResponse before reading .status.

Navigate from codeprogrammatic-navigation

import { useNavigate, useLocation } from 'react-router-dom';

function LogoutButton() {
  const navigate = useNavigate();
  const location = useLocation();
  return (
    <button onClick={async () => {
      await logout();
      navigate('/login', { replace: true, state: { from: location.pathname } });
    }}>Log out</button>
  );
}

Calling navigate() during render throws; do it in an event handler or an effect, or return a redirect() from a loader instead.

Read and write query string statesearch-params

import { useSearchParams } from 'react-router-dom';

function Search() {
  const [params, setParams] = useSearchParams();
  const q = params.get('q') ?? '';
  return (
    <input
      value={q}
      onChange={(e) => setParams(
        (prev) => { prev.set('q', e.target.value); return prev; },
        { replace: true }
      )}
    />
  );
}

setSearchParams pushes a history entry by default, so a search box without replace: true fills the back button with one entry per keystroke.

Guard a route from its loaderprotected-route

import { redirect } from 'react-router-dom';

export async function requireUser({ request }) {
  const user = await getSession();
  if (!user) {
    const url = new URL(request.url);
    throw redirect(`/login?next=${encodeURIComponent(url.pathname)}`);
  }
  return { user };
}

Throwing the redirect from a loader stops the navigation before the component mounts, which avoids the flash you get from redirecting inside a useEffect.

Code-split a routelazy-route

const router = createBrowserRouter([
  {
    path: 'reports',
    lazy: async () => {
      const { Reports, reportsLoader } = await import('./routes/reports');
      return { Component: Reports, loader: reportsLoader };
    },
  },
]);

route.lazy loads the loader and the component in the same chunk, so unlike React.lazy the data fetch is not blocked behind the component download.

Submit without navigatingfetcher-without-navigation

import { useFetcher } from 'react-router-dom';

function FavoriteButton({ id, isFavorite }) {
  const fetcher = useFetcher();
  const optimistic = fetcher.formData
    ? fetcher.formData.get('favorite') === 'true'
    : isFavorite;
  return (
    <fetcher.Form method="post" action={`/items/${id}/favorite`}>
      <button name="favorite" value={String(!optimistic)}>
        {optimistic ? 'Unfavorite' : 'Favorite'}
      </button>
    </fetcher.Form>
  );
}

fetcher.formData is available while the request is in flight, which is what makes the optimistic value above work without extra state.

Drop react-router-dom before v8migrate-off-the-shim

// 1. Rewrite imports across the codebase:
//    npx jscodeshift -t ./rename.js src
//    or a plain search and replace:
//    from 'react-router-dom'  ->  from 'react-router'

import { Link, useLocation, RouterProvider } from 'react-router';

// 2. Then remove the package:
// npm uninstall react-router-dom

In v8 the DOM-specific entry points move to react-router/dom; doing the plain rename while still on v7 makes that upgrade a one-line change.

Alternatives

PackageRegistryPick it when
react-routernpmThe same library without the deprecated shim; this is what every new project should install
@tanstack/react-routernpmYou want fully type-safe routes, typed search params and first-class data loading without a framework
wouternpmA small client-side app that needs hooks-based routing in a couple of KB with no data layer