mrkeyoor.com_
Sun 20 Sept 11:46 UTC
npmWeb Frontendupdated 20 Sept 2026

react-router-dom review

react-router-dom 7 is the compatibility entry point for React Router's browser APIs. It re-exports routing components, hooks, data routers, forms, loaders, actions, fetchers, and DOM-specific RouterProvider behavior while depending on react-router. Version 7.18.2 hardens React Server Components CSRF code paths. This package has reached a boundary: React Router 8 removed react-router-dom after moving most imports to react-router and DOM provider imports to react-router/dom. It remains relevant for v6 and v7 applications, but a new application should start on the current react-router package.

31.4Mdownloads / wk
Verdict

Keep react-router-dom 7.18.2 for a maintained v6 or v7 application and take the CSRF hardening patch. Do not add it to a new app: React Router 8 has already removed the package, so start with react-router.

We installed it

Lab card: what happened when we installed react-router-domScreenshot of react-router-dom documentation
Install✓ · 1.2s7 packages on disk · 13 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser67.5 KBgzipped (207.6 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does react-router-dom install cleanly?

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

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

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

Does react-router-dom work with both ESM and CommonJS?

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

Does react-router-dom include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

react-router-dom or react-router: which should you use?

react-router: Choose it for new React Router work and the v8 path that replaces react-router-dom imports. Keep react-router-dom 7.18.2 for a maintained v6 or v7 application and take the CSRF hardening patch.

When should you not use react-router-dom?

You are choosing a router for a new application. React Router 8 removed this package; install react-router and use react-router/dom for DOM provider exports.

API stability2/5Version 7 preserved many v6 imports through this compatibility package and offered future flags for upcoming behavior, which helps staged migrations. The package itself is absent from version 8, however. Applications must move most imports to react-router and provider imports to react-router/dom, while v8 also raises runtime baselines and makes flagged behavior the default. That is a clear end to this package-level API.
Docs4/5React Router has detailed guides for declarative, data, and framework modes plus API pages for routes, loaders, actions, forms, fetchers, pending UI, testing, and upgrades. Versioned 7.18.2 documentation remains live. The main site now follows v8, so search results can mix removed react-router-dom imports with current react-router examples unless readers deliberately stay in the v7 documentation.
Maintenance4/5The repository was pushed on 2026-08-21, is not archived, and has an active v8 line. The v7 branch still received 7.18.2 on 2026-07-28 to harden RSC CSRF paths after v8 had shipped. GitHub reports 170 open issues and pull requests across the whole monorepo. Maintenance is strong at the project level, but this package is a compatibility endpoint rather than part of the v8 package set.
Ecosystem5/5npm recorded 42,665,485 downloads for the week ending 2026-08-22, and the shared repository has 56,572 stars. The v6 and v7 APIs appear across tutorials, component libraries, application templates, and testing utilities. That reach makes migration support valuable, but the download total includes a large installed base and should not be read as evidence that new projects ought to choose the removed package name.

Use it if

  • You maintain a React Router 6 or 7 application whose imports already come from react-router-dom.
  • Your v7 app needs nested routes, loaders, actions, error boundaries, forms, or fetchers through the browser data router.
  • You are preparing a staged v8 migration and want v7 behavior while changing imports and enabling future flags.
  • Your runtime supports Node 20 or newer and the application already uses React and React DOM 18 or newer.
Skip it if

Setup reality

Our clean Node 22 install of react-router-dom 7.18.2 finished in 1.2 seconds. Seven packages occupied 13 MB afterward. The package is 36 KB unpacked, declares one direct dependency and two peer dependencies, and has an MIT license. npm audit reported zero known vulnerabilities at every severity.

React and React DOM 18 or newer must be supplied by the application. The package itself requires Node 20 or newer. It publishes CommonJS and ESM targets through an exports map; both require() and import worked in our container. TypeScript declarations are bundled. A full namespace browser import measured 207.6 KB minified and 67.5 KB gzipped in our esbuild check, so route-level lazy loading and real application bundle analysis matter.

Data routers change setup beyond matching URLs. createBrowserRouter owns route loaders, actions, pending navigation, revalidation, error elements, and abort signals. A loader can run again after an action or search-parameter change, so it must tolerate repeated calls. Browser loaders also expose requests to users; secrets and privileged database access belong behind authenticated server endpoints.

The current migration trap is package identity. Version 7 keeps react-router-dom so old imports continue to work, while version 8 removes it. Move ordinary imports to react-router and RouterProvider or HydratedRouter to react-router/dom before upgrading. Use the versioned 7.18.2 docs while this dependency remains. Tests need a memory router or createMemoryRouter rather than BrowserRouter when no real browser history exists.

Patterns

Create a v7 browser data router create-browser-router

import { createBrowserRouter, RouterProvider } from 'react-router-dom'

const router = createBrowserRouter([
  { path: '/', element: <Home /> },
  { path: '/about', element: <About /> },
])

root.render(<RouterProvider router={router} />)

These imports work in v7. For v8, createBrowserRouter comes from react-router and RouterProvider comes from react-router/dom.

Render child routes through an outlet define-nested-routes

import { Outlet } from 'react-router-dom'

const routes = [{
  path: '/account',
  element: <AccountLayout />,
  children: [
    { index: true, element: <Profile /> },
    { path: 'security', element: <Security /> },
  ],
}]

function AccountLayout() {
  return <main><AccountNav /><Outlet /></main>
}

Child paths without a leading slash are relative to the parent route.

Load data before rendering a route load-route-data

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

async function projectLoader({ params, request }) {
  const response = await fetch('/api/projects/' + params.projectId, {
    signal: request.signal,
  })
  if (!response.ok) throw new Response('Project not found', { status: 404 })
  return response.json()
}

function Project() {
  const project = useLoaderData()
  return <h1>{project.name}</h1>
}

Forward request.signal so an abandoned navigation can cancel the fetch. The API must still enforce authorization.

Handle a mutation with Form and an action submit-route-action

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

async function renameAction({ request, params }) {
  const form = await request.formData()
  await api.renameProject(params.projectId, String(form.get('name')))
  return redirect('/projects/' + params.projectId)
}

function RenameForm() {
  return <Form method="post"><input name="name" /><button>Save</button></Form>
}

An action normally triggers loader revalidation. Validate input and authorization on the server reached by the action.

Show pending navigation feedback show-navigation-state

import { useNavigation } from 'react-router-dom'

function SaveStatus() {
  const navigation = useNavigation()
  return navigation.state === 'idle'
    ? null
    : <p aria-live="polite">Working...</p>
}

The state can be loading or submitting. Keep existing content visible unless the new route truly requires a full replacement.

Submit without changing the current route fetch-without-navigation

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

function FavoriteButton({ projectId, active }) {
  const fetcher = useFetcher()
  return (
    <fetcher.Form method="post" action={'/projects/' + projectId + '/favorite'}>
      <button name="active" value={active ? '0' : '1'} disabled={fetcher.state !== 'idle'}>
        {active ? 'Unfavorite' : 'Favorite'}
      </button>
    </fetcher.Form>
  )
}

A fetcher runs route data logic without navigation and has its own state. Concurrent fetchers can finish out of order.

Link within a nested route link-relative-route

import { Link, NavLink } from 'react-router-dom'

<Link to="security">Security</Link>
<NavLink to="." end className={({ isActive }) => isActive ? 'active' : undefined}>
  Overview
</NavLink>

Relative links resolve against the route hierarchy. end prevents the parent link from staying active on every child path.

Read a dynamic path segment read-route-params

import { useParams } from 'react-router-dom'

function ProjectPage() {
  const { projectId } = useParams()
  return <Project id={projectId} />
}

Path parameters are strings or undefined. Parse and validate them before using them as numeric IDs or trusted input.

Store a filter in the URL update-search-params

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

function SearchBox() {
  const [params, setParams] = useSearchParams()
  const query = params.get('q') || ''
  return <input value={query} onChange={(event) => {
    setParams((current) => {
      current.set('q', event.target.value)
      return current
    }, { replace: true })
  }} />
}

Changing search parameters is navigation and can re-run loaders. replace avoids adding one history entry per keystroke.

Navigate after application logic navigate-programmatically

import { useNavigate } from 'react-router-dom'

function DoneButton() {
  const navigate = useNavigate()
  return <button onClick={() => navigate('/dashboard', { replace: true })}>Done</button>
}

Prefer Link for ordinary navigation so browser link behavior remains available. useNavigate fits event-driven redirects.

Give a route its own error UI render-route-error

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

function RouteError() {
  const error = useRouteError()
  if (isRouteErrorResponse(error)) {
    return <h1>{error.status}: {error.statusText}</h1>
  }
  return <h1>Unexpected route error</h1>
}

const route = { path: '/reports', loader: reportsLoader, element: <Reports />, errorElement: <RouteError /> }

The nearest route errorElement handles thrown responses and rendering errors below that route.

Test a route without browser history test-with-memory-router

import { createMemoryRouter, RouterProvider } from 'react-router-dom'

const router = createMemoryRouter(routes, {
  initialEntries: ['/projects/42'],
})
render(<RouterProvider router={router} />)

A memory router keeps history in process and can start at the exact URL needed by the test.

Alternatives

PackageRegistryPick it when
react-routernpmChoose it for new React Router work and the v8 path that replaces react-router-dom imports.
@tanstack/react-routernpmChoose it when route-tree type inference, typed search parameters, and compile-time navigation checks are priorities.
wouternpmChoose it for a small client-side React router with hooks and minimal data-loading opinions.
universal-routernpmChoose it for framework-neutral route matching when React-specific components and data APIs are unnecessary.

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.