Skip to content
Home
React Router Guide: Routing, Navigation, and Data Loading

React Router Guide: Routing, Navigation, and Data Loading

React React 8 min read 1698 words Beginner ExcellentWiki Editorial Team

React Router is the de facto standard for client-side routing in React applications. Version 6, released in 2021, introduced a completely redesigned API based on React hooks and a hierarchical route configuration that aligns with React’s component model. As Ryan Florence, co-creator of React Router, explained in the v6 announcement: “We built v6 to feel like it was always meant to be part of React.”

React Router v6 Architecture

React Router v6 shifts from a static route configuration to a dynamic, component-based approach. Routes are defined as components rendered inside a <Routes> parent, which automatically selects the best matching route:

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="/users/:id" element={<UserProfile />} />
</Routes>

Nested Routes

Nested routes are one of v6’s killer features. They mirror the component hierarchy and enable layout routes that wrap child content:

<Route path="dashboard" element={<DashboardLayout />}>
  <Route index element={<DashboardHome />} />
  <Route path="settings" element={<Settings />} />
</Route>

The <DashboardLayout /> component renders an <Outlet /> where child route content appears. This eliminates the manual rendering of nested content that plagued v5 and earlier versions.

Route Matching Algorithm

React Router v6 uses a ranked route matching algorithm. It scores routes based on specificity: static segments, dynamic segments (:param), and wildcards (*). The most specific match wins. This approach, documented in the React Router v6 upgrade guide, eliminates the ambiguity of v5’s “first match wins” behavior.

Core Hooks

React Router v6 provides hooks for accessing routing state:

  • useParams — Returns URL parameters from the current route
  • useNavigate — Programmatic navigation function (replaces useHistory)
  • useLocation — Returns the current location object
  • useSearchParams — Read and modify query parameters (similar to useState for the query string)
  • useOutlet — Returns the child route element for the current route
  • useHref — Returns the resolved href for a given path

useNavigate

const navigate = useNavigate();
navigate('/dashboard', { replace: true });

navigate accepts a relative or absolute path, a delta number (-1 for back), or a state object. The replace option replaces the current history entry instead of pushing a new one.

useSearchParams

const [searchParams, setSearchParams] = useSearchParams();
const page = searchParams.get('page') || '1';
setSearchParams({ page: '2' });

This is particularly useful for filter and pagination UIs where query parameters should reflect the current view.

Data Loading with Loaders and Actions

React Router v6.4 introduced data APIs — loaders, actions, and fetchers — that integrate data fetching with routing. These APIs were extracted from Remix, the full-stack React framework created by the React Router team.

Loaders

A loader is an async function that provides data to a route before it renders:

<Route
  path="users/:id"
  element={<UserProfile />}
  loader={({ params }) => fetch(`/api/users/${params.id}`)}
/>

Access loaded data with useLoaderData:

function UserProfile() {
  const user = useLoaderData();
  return <div>{user.name}</div>;
---

Loaders enable concurrent data fetching for all matched routes, eliminating the waterfall pattern common in useEffect-based data fetching. The React Router documentation explains that loaders run in parallel for sibling routes, and parent loaders run before their children.

Actions

Actions handle mutations (form submissions, deletions) and automatically revalidate loader data:

<Route
  path="users/:id/edit"
  element={<EditUser />}
  action={async ({ request, params }) => {
    const formData = await request.formData();
    return updateUser(params.id, Object.fromEntries(formData));
  }}
/>

Use useActionData to access the result of the most recent action submission, and useNavigation to track the submission state.

Navigation Guards

React Router v6 provides <Navigate> for declarative redirects and the useBlocker hook (experimental) for preventing navigation. The shouldRevalidate option on loaders gives fine-grained control over when data refreshes.

Protected Routes

Implement authentication guards with a wrapper component:

function RequireAuth({ children }) {
  const user = useAuth();
  return user ? children : <Navigate to="/login" replace />;
---

<Route path="/dashboard" element={<RequireAuth><Dashboard /></RequireAuth>} />

Lazy Loading Routes

React Router v6 integrates with React.lazy for route-level code splitting. Each route’s component is loaded only when the user navigates to it:

const Dashboard = React.lazy(() => import('./routes/Dashboard'));
const Settings = React.lazy(() => import('./routes/Settings'));

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={
        <Suspense fallback={<PageSkeleton />}>
          <Dashboard />
        </Suspense>
      } />
      <Route path="/settings" element={
        <Suspense fallback={<PageSkeleton />}>
          <Settings />
        </Suspense>
      } />
    </Routes>
  );
---

Route-level code splitting reduces the initial bundle size by only loading the components needed for the current page. This improves initial load time and can reduce time-to-interactive by 30-50% on slower networks. Combined with React Router’s data loaders, which can fetch data in parallel with the component code, users experience minimal loading time when navigating between routes.

Error Handling with Error Boundaries

React Router v6’s data router API integrates with error boundaries at the route level:

<Route
  path="users/:id"
  element={<UserProfile />}
  errorElement={<ErrorBoundary />}
  loader={({ params }) => fetchUser(params.id)}
/>

Error elements replace the routing component when a loader or action throws an error. This provides granular error handling — a failed data fetch in one route does not crash the entire application, and each route can have its own error UI.

function ErrorBoundary() {
  const error = useRouteError();
  return <div>Something went wrong: {error.message}</div>;
---

The useRouteError hook provides access to the thrown error for logging and user-friendly messages. Combine with isRouteErrorResponse to distinguish between application errors and 404/redirect responses.

Code Splitting with React Router

Combine React Router with React.lazy for route-based code splitting:

const Dashboard = React.lazy(() => import('./Dashboard'));
<Route path="/dashboard" element={
  <Suspense fallback={<Loading />}>
    <Dashboard />
  </Suspense>
--- />;

Route-based code splitting reduces initial bundle size by loading components only when their route is visited. The React documentation on code splitting recommends this as a best practice for production applications.

Link and Navigation Components

React Router provides <Link> for declarative navigation and <NavLink> for active link styling:

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

<Link> renders an <a> element with the correct href, which ensures right-click and open-in-new-tab work correctly — a benefit over programmatic navigation. The prefetch prop on <Link> (when used with data routers) prefetches the linked route’s data for instant navigation.

Relative links in React Router v6 resolve relative to the current route, not the URL. This means <Link to="edit"> inside a /users/123 route navigates to /users/123/edit, while <Link to="../settings"> navigates to /settings.

Scroll Restoration

React Router v6 provides scroll restoration by default, restoring scroll position when navigating with the back/forward buttons. For custom scroll behavior, use useScrollRestoration (experimental) or manually manage scroll positions with useEffect and useLocation:

function ScrollToTop() {
  const { pathname } = useLocation();
  useEffect(() => window.scrollTo(0, 0), [pathname]);
  return null;
---

For list/detail patterns where you want to preserve scroll position in a list when navigating to a detail page and back, React Router’s data loading integrates with the browser’s scroll cache when using ScrollRestoration from the data router APIs.

URL Parameter Patterns

React Router supports several parameter patterns:

  • Required params: :id — must be present in the URL
  • Optional params: :page? — made optional with a ? suffix (not supported directly; use default values via useParams)
  • Splat params: * — matches everything after the route path
  • Pattern matching: Use parameters inside useParams and combine with useSearchParams for complex filtering
  • Search parameter synchronization: Keep URL query parameters in sync with component state to enable shareable links and browser history navigation for filter, search, and pagination state

React Performance Best Practices

Performance optimization in React requires understanding when and why components re-render. React re-renders a component when its state changes, its parent re-renders, or the context it consumes changes. Unnecessary re-renders are the most common performance issue. Use React.memo to prevent re-renders when props have not changed — it performs a shallow comparison of props and skips rendering if they are identical. The useMemo hook memoizes expensive computation results, recalculating only when dependencies change. The useCallback hook memoizes function references, preventing child components from re-rendering due to new function references on every parent render. For lists, ensure each item has a stable, unique key prop — using array indices as keys causes bugs when items are reordered. Virtualization libraries like react-window and react-virtuoso render only visible items in long lists, dramatically reducing DOM nodes. Code splitting with React.lazy and Suspense loads components on demand, reducing initial bundle size. Profile your application with React DevTools Profiler before optimizing — measuring identifies actual bottlenecks that are worth fixing.

Testing React Components Effectively

React Testing Library encourages testing from the user’s perspective. Write tests that verify behavior, not implementation details. Use getByRole, getByLabelText, and getByText queries to find elements the way users do — by accessibility attributes and visible text. Avoid testing internal state or component internals; instead, test what the user sees and does. For async operations, use waitFor and findBy queries that retry until the element appears. Mock external dependencies at the network level using MSW (Mock Service Worker) rather than mocking imported modules. This creates more realistic tests that verify your integration with actual API contracts.

FAQ

What happened to Switch in React Router v6?

<Switch> was replaced by <Routes>. <Routes> uses the new ranked route matching algorithm and automatically selects the best match, so you no longer need to order routes from most to least specific.

How do I pass props to a route component?

Use the element prop to render your component directly with any props:

<Route path="/users" element={<UserList currentUser={user} />} />

Can I use React Router outside the browser?

Yes. React Router v6 includes createMemoryRouter for testing and non-browser environments. The createStaticRouter API supports server-side rendering.

What is the difference between React Router and Remix?

Remix is a full-stack web framework built on top of React Router. It uses React Router’s data APIs (loaders, actions) as its foundation and adds server-side rendering, file-based routing, and deployment adapters. React Router is the client-side routing library that powers Remix.

Conclusion

React Router v6 is a complete reimagining of client-side routing that aligns with React’s component model and hooks API. The addition of loaders and actions in v6.4 brings data fetching into the routing layer, reducing boilerplate and improving performance through parallel loading. The official React Router documentation at reactrouter.com provides excellent tutorials and API references. For managing form state alongside routing, see React Forms Guide, and for authentication patterns that integrate with protected routes, see React Authentication.

For a comprehensive overview, read our article on Next Js Guide.

For a comprehensive overview, read our article on React Authentication.

Section: React 1698 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top