React Router vs React Router DOM: An In-Depth Comparison

A detailed, analytical comparison of React Router and React Router DOM, outlining when to use each, key API differences, SSR implications, and best practices for modern React web apps.

WiFi Router Help
WiFi Router Help Team
·5 min read
Router Showdown - WiFi Router Help
Photo by vlrondonvia Pixabay
Quick AnswerComparison

React Router DOM is the web binding for the core React Router. For most web apps, use react-router-dom because it includes BrowserRouter, Routes, and web-specific navigation utilities. The core react-router package exposes the routing primitives (Router, Route, Link, useNavigate) and can be paired with alternative renderers when you’re not building a standard web app. In short: web apps use DOM bindings; non-web renderers use the core API.

Core difference: React Router vs React Router DOM

At its core, React Router is the routing library, providing navigation primitives and state management. React Router DOM is the web bindings that adapt those primitives to the browser environment, offering DOM-aware components and helpers. This separation mirrors a common React pattern: a renderer-agnostic core paired with platform-specific bindings. In practice, the web-focused package exposes BrowserRouter, HashRouter, Link, and NavLink, while the core library focuses on reusable concepts that can pair with other renderers, including React Native via react-router-native. The practical upshot is clear: the core API stays consistent across platforms, while the DOM bindings optimize for web apps with a browser history API, scroll restoration, and DOM event handling.

Understanding this split helps teams reason about where routing concerns live. If you write against the core primitives, you gain portability across renderers. If you work through the DOM bindings, you get a convenient, browser-optimized API surface that’s widely documented and tested. For teams, this separation also clarifies responsibilities: business logic and routing rules live in the core, while the web layer handles UI composition and navigation. Finally, modern React Router emphasizes API consistency across renderers, with the DOM bindings extending the core experience without redefining the underlying concepts.

Relationship and package structure

In a typical React project you’ll encounter two packages: react-router (the core routing library) and react-router-dom (the web bindings that wire the core into the browser environment). In practice, most projects install react-router-dom, which brings in react-router as a dependency. The DOM package re-exports the core primitives with a web-oriented surface, including components like BrowserRouter and Link, and hooks such as useNavigate and useLocation. If you’re building a non-web renderer (for example React Native), you’ll swap in the corresponding native bindings, while still relying on the core routing concepts. This modular approach keeps the core navigation logic portable, while enabling a rich, developer-friendly web API. When you write code, you’ll typically import from react-router-dom for web apps, and you’ll often import only from react-router for non-web adapters or for lower-level demonstrations of routing concepts.

When to use each: web apps vs non-web renderers

For standard web applications, react-router-dom is the recommended choice. It provides BrowserRouter, Routes, and Link/NavLink components that map directly to browser history, URL segments, and user navigation. If you’re building an app that doesn’t render in a browser (such as a React Native app or a custom renderer), the core react-router package offers the same routing primitives without the DOM bindings. In practice, many teams start with react-router-dom to cover the majority of web use cases and then extract routing logic to the core when they need cross-platform reuse or to publish a reusable library. The key takeaway: web apps rely on DOM bindings for simplicity and ecosystem alignment, while non-web environments lean on the core tooling and potentially platform-specific bindings.

API surface: core primitives vs DOM bindings

Because react-router and react-router-dom share a lineage, the difference is mostly about surface area and ergonomics. The core library provides the essential navigation primitives — properties like location, history, and route matching — that can be used in any renderer. The DOM bindings extend this with browser-specific components and helpers that streamline common patterns: BrowserRouter or HashRouter create a routing context, Routes defines a set of Route elements, and Link or NavLink handles declarative navigation. In v6, there are additional hooks such as useParams, useLocation, and useNavigate that exist in both ecosystems, but how you render them depends on the bindings you choose. A practical rule: if your code is web-centric, import from react-router-dom to get the DOM-friendly components; if you’re building a library that targets a non-web renderer, rely on the core primitives and adapters.

Routing concepts: nested routes, layouts, and dynamic routes

Routing in both packages revolves around the idea of a tree of routes and data-driven navigation. Nested routes allow you to compose UI from smaller pieces while preserving URL structure. Layout routes give you stable chrome around content that changes with the route. Dynamic routes handle parameters like /users/:id, enabling on-demand data fetching and stateful rendering. The web bindings add convenience components to express these patterns directly in JSX, while the core offers the same concepts in a renderer-agnostic form. When designing an app, plan your route hierarchy upfront, then map that structure to components using Routes and Route in react-router-dom, or to analogous primitives in the core if you’re building a cross-platform library.

SSR, hydration, and server rendering considerations

Server-side rendering (SSR) and hydration bring extra considerations for routing. React Router DOM provides a server-side module (react-router-dom/server) in modern versions to help with pre-computed routes and streaming rendering. The core library remains renderer-agnostic, which means SSR implementation often relies on bindings that understand how to serialize route state and hydrate on the client. When you build SSR-capable pages, use the DOM server bindings for web apps to enable consistent routing behavior during initial render and client hydration. For non-web environments, SSR strategies depend on the chosen renderer and any adapters you implement for the core routing primitives.

Migration paths and best practices for v5 to v6

Migrating from older versions (like v5) to v6 typically involves adopting the Routes component, replacing Switch with Routes, and replacing Redirect with Navigate. You’ll also notice a shift from useHistory to useNavigate and from withRouter HOCs to hook-based patterns. The DOM bindings simplify these changes by providing more explicit routing elements and clearer error messages. Best practices include migrating one feature area at a time, using memory routing in tests, and aligning on a single binding for your web apps to minimize drift. If you publish a reusable library, prefer the core primitives and provide adapters so consumers can plug in their preferred bindings.

Testing and debugging strategies

Testing routes requires thoughtful mocks and utilities. MemoryRouter and createMemoryHistory are commonly used to simulate navigation in unit tests. When testing web apps, React Testing Library paired with user-event helps you exercise navigation and URL changes in a realistic manner. Debugging routing issues often comes down to understanding how route matching works, how parameters are passed, and how redirects affect the user flow. In practice, keep tests focused on navigation outcomes (paths reached, params captured) rather than UI details, and use mocked fetches or data loaders to isolate route logic from data fetching. The DOM bindings provide helpful testing utilities that mirror real user interactions, while the core primitives require adapters suitable for your renderer.

Practical setup: minimal app example

Below is a minimal web app setup using react-router-dom v6 to illustrate a simple three-page structure. The code demonstrates BrowserRouter, Routes, and basic navigation. You can copy this into a React app and adapt it to your routes:

JSX
import React from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; function Home(){ return <h1>Home</h1>; } function About(){ return <h1>About</h1>; } function App(){ return ( <BrowserRouter> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> </nav> <Routes> <Route path="/" element={<Home/>} /> <Route path="/about" element={<About/>} /> </Routes> </BrowserRouter> ); } createRoot(document.getElementById('root')).render(<App/>);

This example demonstrates a standard DOM-based router, with room to add nested routes, redirects, and optional data loaders as your app grows.

Common pitfalls and anti-patterns

Avoid mixing core and DOM bindings in the same component tree without a clear adapter layer. Don’t rely on global state to drive navigation without considering how URLs reflect UI state. Be cautious with deep nesting: overly nested routes can degrade readability and complicate data loading. Finally, avoid using the DOM-specific components for non-web renderers; keep renderer-agnostic code in the core and provide a clean adapter for each target platform. Practically, keep your route definitions declarative, testable, and aligned with a single binding strategy to reduce maintenance overhead.

Authoritative sources and further reading

  • Official React Router documentation (react-router-dom): https://reactrouter.com/
  • MDN Web Docs: Single-page applications (SPA) and routing concepts: https://developer.mozilla.org/en-US/docs/Glossary/Single-page_application
  • Google Developers: SPA navigation patterns and routing concepts: https://developers.google.com/web/fundamentals/primers/spa-navigation

Comparison

Featurereact-routerreact-router-dom
Package roleCore routing primitivesWeb bindings with DOM integration
Primary UI componentsCore primitives (Router, Route) and routing hooksBrowserRouter, Routes, Link, NavLink
API surface focusPlatform-agnostic navigation primitivesWeb-specific wrappers and helpers
SSR/server renderingRequires platform adapters to render on the serverOfficial server bindings (react-router-dom/server) for SSR workflows
Usage patternLibrary usage or adapter-based implementationsDirectly for standard web apps
Migration considerationsRequires adapters for non-web renderersMigration to v6 emphasizes Routes and Navigate
Bundle impactSmaller surface with core onlyCommonly bundled with DOM bindings for web apps

Benefits

  • Clear separation of core logic and DOM bindings
  • Easier cross-platform adaptation and future-proofing
  • Web-specific components accelerate web app development
  • Strong ecosystem and documentation across web renderers
  • Stability and consistency of APIs across renderers

The Bad

  • Adds an extra dependency for web projects
  • Can confuse beginners about which package to import
  • Migration from older versions can require refactoring
  • Dual maintenance when targeting both web and non-web renderers
Verdicthigh confidence

Prioritize react-router-dom for web apps; use the core react-router primitives when you need cross-platform routing or custom renderers.

For most modern React web apps, the DOM bindings provide a complete, well-supported solution. The core package remains valuable for libraries and non-web renderers. The recommended approach is to adopt react-router-dom for web-focused development and reserve react-router for cross-platform scenarios or advanced customization.

People Also Ask

What is the difference between react-router and react-router-dom?

React Router is the core library with routing primitives. React Router DOM is the web binding that adds browser-specific components for web apps. For most projects, use the DOM bindings when building a web app, and reserve the core primitives for non-web environments or library authors.

Core routing primitives form the foundation; for web apps, stick with the DOM bindings for a browser-friendly API.

Can I use react-router without react-router-dom in a web app?

Not recommended. The web bindings provide essential DOM components and browser integration that the core library does not supply by itself. You can theoretically build a web app with only the core library using a custom renderer, but it’s impractical for typical use cases.

If you’re building for the web, you’ll want the DOM bindings for a complete experience.

Is react-router-dom SSR supported?

Yes, modern versions support SSR through the react-router-dom/server module, enabling server-side route rendering and proper hydration on the client. This makes it suitable for SEO-friendly, fast initial renders in web apps.

Yes, there are server bindings you can use for SSR.

What changed from React Router v5 to v6?

Key changes include switching from Switch to Routes, Redirect to Navigate, and a more explicit nesting model with layout routes. The API surface favors hooks and a simplified item structure, improving predictability and composability.

v6 made routing simpler with Routes and Navigate, and better nested routing.

Do I need to upgrade both packages at the same time?

Typically yes, to keep API compatibility between the core and the DOM bindings. Upgrading one without the other can lead to mismatched expectations and runtime errors. Always test in a staging environment.

Usually upgrade together to avoid API mismatches.

How should I handle nested routes effectively?

Nested routes are powerful for layouts and shared UI but can become complex. Plan a clear hierarchy, use layout routes to wrap common chrome, and leverage index routes for default content to keep the structure readable.

Plan your nesting carefully to keep routes readable.

What to Remember

  • Prefer react-router-dom for web apps
  • Core primitives enable cross-platform routing
  • Use Navigate and Routes in v6 for clean patterns
  • SSR and server routing are better supported via DOM server bindings
  • Keep a single binding strategy to reduce complexity
Comparison infographic showing core vs web bindings
Illustrative comparison of core vs DOM bindings.

Related Articles