Mastering Routing in React with React Router v6

Building multi-page React applications requires a robust routing solution that translates URL paths into components on screen. The gold standard for declarative routing in React is React Router DOM.

Over 75% of large React applications and 50% of smaller projects rely on React Router, making it the most popular routing library by far according to the latest State of JavaScript surveys.

This definitive guide will teach you how to master routing in your React apps leveraging the power and flexibility of React Router v6.

Why Routing Matters in React

Let‘s first understand why routing is so essential for crafting dynamic, interactive React applications.

Routing enables you to:

  • Break down UIs into modular, reusable components mapped to paths
  • Keep UI state encapsulated for each view
  • Manage navigation flow and transitions
  • Change URL to control browser history
  • Render UI responsive to URL changes
  • Nest layouts and compose complex UIs
  • Integrate data loading lifecycles with location
  • And more…

Without routing in place, changes in UI state and user navigation quickly become unmanageable.

Some examples of where routing supercharges your React architecture:

  • Multi-step forms or workflows
  • Dashboards with data visualization
  • Feature areas that load async data
  • Public vs private app sections
  • Logging users in/out to control access

And as apps grow to 50+ screens, routing is what keeps UIs manageable and flexible.

Now the only question is which routing approach to use…

Battle of React Routers

The React ecosystem has produced many competitive router implementations over the years. Here is how the most popular solutions stack up:

Library Stars Size SSR
react-router-dom 35k 16kB gzipped Client-only
Next.js Router 33k Full SSR
Reach Router 4k 7kB gzipped Client-only
Remix Router 14k Partial SSR

So why has react-router-dom won out as the routing tool of choice for most React developers?

A few reasons:

✅ Lightweight yet fully-featured
✅ Excellent documentation
✅ Huge community adoption
✅ Flexible and extensible API

It continues to evolve with each major React release. The latest v6 version modernizes React Router while retaining compatibility.

For traditional SPA projects without a backend server component, react-router-dom remains the standard.

Now onto the code!

Installing React Router v6

To get started with React Router in your application:

npm install react-router-dom

Or

yarn add react-router-dom

This will install the package for you to import router components from.

Here are the core imports you‘ll leverage:

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

This provides:

  • BrowserRouter – Enables routing capabilities
  • Routes – Defines route hierarchy
  • Route – Maps path to UI component
  • Link – Generates navigation between routes

We‘ll explain how these pieces fit together next.

Router Concepts

At a high level, here is how React Router operates:

When the URL updates in the browser address bar:

  1. React Router checks the Routes configuration you define
  2. Finds the first Route path that matches
  3. Displays that route‘s element on screen
  4. Updates UI without full page refresh

Under the hood, React Router taps into the browser‘s built-in History API manipulating state and the navigation stack to control transitions between routes.

But enough theory – let‘s jump into some code!

Defining Routes

The starting point for wiring up React router is mapping URL paths to components.

Here is a simple App component with two route definitions:

import { BrowserRouter, Routes, Route } from ‘react-router-dom‘;
import { Home, About } from ‘./pages‘;

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

Breaking this down:

  • BrowserRouter is the root wrapping all routes
  • Routes container to define routes
  • Each Route maps a path to display element
  • Elements can be React components

Now:

  • When URL is /<Home> renders
  • When URL is /about<About> renders

This allows you to toggle UI views as the URL updates!

Linking Between Routes

Once you have some routes defined, you need a way for users to navigate between them.

For this, use the <Link> component:

import { Link } from ‘react-router-dom‘;

function Home() {
  return (
    <div>

      <Link to="/about">About</Link> 
    </div>
  );  
}

function About() {
  return ; 
}

Now when a user clicks the <Link>, React Router will:

  • Change URL to /about
  • Match <About> route
  • Render the <About> component 😎

Plus, <Link> handles many useful features out of the box:

  • Accessible focus management
  • Prefetching optimizations
  • Relative vs absolute paths
  • URL generation helpers

This handles navigational heavy lifting so you don‘t have to.

Dynamic Route Parameters

Hardcoding exact path matches works for simple cases. But often you need dynamic, parameterized routes that accept variables.

Say you have a user profile page to render based on an id:

/users/1234 
/users/5678

You can define a route parameter like so:

<Route path="/users/:userId" element={<UserProfile />} />  

Then inside the UserProfile component, you can access that userId value using the useParams hook:

import { useParams } from ‘react-router-dom‘;

function UserProfile() {
  const { userId } = useParams();

  return 
}

Now as the :userId portion of the URL changes, UserProfile will update accordingly!

Composing Layouts with Nested Routes

Complex applications often require multi-level UI nesting.

React Router makes composing layouts simple using nested routes.

Say you have a dashboard with analytics child pages:

+-- /dashboard 
    |
    +-- analytics
         |  
        +-- revenue
        |
        +-- traffic   
         |
        +-- reports

You can model this with:

function App() {
  return (
    <Routes>
      <Route 
        path="/dashboard" 
        element={<DashboardLayout />}
      >
        <Route path="analytics">
          <Route path="revenue" element={<AnalyticsRevenue />} />  
          <Route path="traffic" element={<AnalyticsTraffic /> />
        </Route>
      </Route>
    </Routes>
  );
}

function DashboardLayout() {
  return (
    <div>
      {/* Sidebars/Nav */}
      <Outlet />
    </div>
  ) 
}

Here is what happens as you navigate:

  1. When on /dashboard – Shows <DashboardLayout> only
  2. On URL /dashboard/analytics – Shows BOTH <DashboardLayout> and nested <Outlet>
  3. When on deeper nested route – Shows parent layout + matched nested route element

This pattern is immensely powerful for handling page variability + shared containers.

Authentication and Access Control

Controlling access to protected routes is easy with React Router.

A common pattern is to check auth globally before rendering child route elements:

function ProtectedRoute({ children }) {

  const isAuthenticated = checkAuth();

  if (!isAuthenticated) {
    // If not authenticated - redirect
    return <Navigate to="/login" />; 
  }

  // Authenticated - render child routes 
  return children;
}

function App() {

  return (
    <Routes>
      <Route element={<ProtectedRoute />}>
        {/* Protected routes here */}
      </Route>

      <Route path="/login" element={<Login />} />
    </Routes>
  )

}

Now any route wrapped via <ProtectedRoute> requires valid authentication!

Some other authorization patterns:

  • Granular access per role
  • Mix public/private routes
  • Token-based authentication

Proper access control is essential as applications grow.

Code Splitting + Performance

When dealing with many routes, you‘ll want to optimize load performance.

A great way to speed up initial rendering is code splitting – splitting bundle output into separate chunks that load on demand.

Here is how to lazily load routes with React Suspense:

const Dashbaord = React.lazy(() => import(‘./Dashboard‘));

function App() {

  return (
    <Suspense fallback={<Loader />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />  
      </Routes>
    </Suspense>
  )
}

Now the Dashboard component bundle splits out into its own JavaScript chunk.

This chunk only loads when user navigates to /dashboard route resulting in faster first paint!

Recap and Next Steps

Hopefully you now feel empowered to tackle routing in your React applications!

Here are some parting thoughts:

  • Experiment with diverse routing patterns
  • Split route definitions into logical domain groupings
  • Carefully plan navigation UX and access control early on
  • Add route animations to delight users
  • Monitor bundle sizes and lazy load deferred routes

This is just the beginning of mastering routing. Many additional concepts around query parameters, SSR rendering, custom route matching, and more that build on these foundations.

But absorbing these core principles will guide you far.

For any other questions reach out in the comments!