BeginnerReact Concepts

React Router – Like Navigation Boards on the Web Highway

React Router is the standard for navigation in React apps. Learn how to set up multiple routes, use BrowserRouter, HashRouter, and MemoryRouter, and enable SPA navigation without full page reloads

By Rudraksh Laddha

Introduction

Two years ago, I inherited a React app with 15 different pages all crammed into a single component with conditional rendering. Every navigation action triggered a full re-render of the entire app state. The performance was terrible, and adding new pages meant touching a 800-line switch statement.

  • → Users couldn't bookmark specific pages
  • → Back button didn't work as expected
  • → No way to share direct links to content

React Router solved all of these problems in one afternoon of refactoring.

React Router transforms your single-page application into a navigation system that feels like a traditional multi-page website, but with all the performance benefits of client-side rendering.


What React Router Actually Solves

React Router isn't just about routing - it's about creating predictable, shareable application states.

In production, this means:

  • URLs that map directly to application state
  • Deep linking that works for user onboarding flows
  • Browser history that behaves as users expect
  • Code splitting opportunities at route boundaries

The Performance Problem with Traditional Links

I learned this lesson when our analytics showed a 3-second average page load time. Every <a href="/page"> tag was:

  • Destroying the entire React component tree
  • Re-downloading JavaScript bundles
  • Losing all application state
  • Re-initializing API connections

React Router's JavaScript-based navigation preserves your app's context while updating the view. Our page transitions dropped from 3 seconds to 50 milliseconds.


Choosing the Right Router for Production

1. BrowserRouter – My Go-To for Most Apps

// Import from react-router-dom
import { BrowserRouter, Routes, Route } from "react-router-dom";

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/dashboard/*" element={<Dashboard />} />
    <Route path="/user/:id" element={<UserProfile />} />
  </Routes>
</BrowserRouter>
  • ✅ Clean URLs that users can bookmark and share
  • ✅ SEO-friendly for server-side rendering
  • ❗ Requires server configuration to handle client-side routes

I use this for 90% of web applications. The server setup is usually just one nginx rule or Express middleware.

2. HashRouter – For Static Deployment Constraints

// HashRouter example
import { HashRouter, Routes, Route } from "react-router-dom";

<HashRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/contact" element={<Contact />} />
  </Routes>
</HashRouter>
  • ✅ Works with any static hosting (GitHub Pages, S3, etc.)
  • ✅ No server configuration required
  • 🚫 URLs look unprofessional with # symbols
  • 🚫 Limited SEO capabilities

I only use this when client constraints prevent server configuration. The UX trade-off is significant.

3. MemoryRouter – For Testing and Embedded Apps

// MemoryRouter for testing
import { MemoryRouter, Routes, Route } from "react-router-dom";

// Perfect for unit tests
<MemoryRouter initialEntries={["/dashboard", "/profile"]} initialIndex={0}>
  <Routes>
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/profile" element={<Profile />} />
  </Routes>
</MemoryRouter>
  • ✅ Ideal for Jest tests and Storybook stories
  • ✅ Works in React Native or Electron apps
  • 🚫 No browser URL synchronization

This is my secret weapon for testing complex routing logic without browser dependencies.


Production Patterns I Use Daily

1. Route-Based Code Splitting

// Route-based code splitting
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const UserProfile = lazy(() => import('./UserProfile'));

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/dashboard" element={
    <Suspense fallback={<div>Loading...</div>}>
      <Dashboard />
    </Suspense>
  } />
</Routes>

This pattern reduced our initial bundle size by 60%. Users only download the code for routes they actually visit.

2. Nested Routes for Complex Apps

<Route path="/settings" element={<SettingsLayout />}>
  <Route index element={<SettingsOverview />} />
  <Route path="profile" element={<ProfileSettings />} />
  <Route path="billing" element={<BillingSettings />} />
  <Route path="team" element={<TeamSettings />} />
</Route>

Nested routes let me build complex layouts with shared navigation and persistent state. The parent component handles common UI while children handle specific functionality.

3. Programmatic Navigation with Context

// Custom hook for navigation
import { useNavigate, useLocation } from "react-router-dom";

const useAuthRedirect = () => {
  const navigate = useNavigate();
  const location = useLocation();

  const redirectAfterLogin = () => {
    const returnTo = location.state?.from || '/dashboard';
    navigate(returnTo, { replace: true });
  };

  return { redirectAfterLogin };
};

I wrap navigation logic in custom hooks to handle complex flows like authentication redirects and form wizards.

4. Dynamic Route Parameters with Validation

// Route parameter validation
import { useParams, Navigate } from "react-router-dom";

const UserProfile = () => {
  const { userId } = useParams();
  
  // Validate route params in production
  if (!userId || !/^\d+$/.test(userId)) {
    return <Navigate to="/users" replace />;
  }

  return <div>User {userId}</div>;
};

Always validate route parameters. Invalid URLs can crash components or expose security vulnerabilities.

5. Route Protection with Custom Hooks

// Protected route component
const ProtectedRoute = ({ children, requiredRole }) => {
  const { user, isLoading } = useAuth();
  const location = useLocation();

  if (isLoading) return <LoadingSpinner />;
  
  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  if (requiredRole && !user.roles.includes(requiredRole)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return children;
};

Route guards prevent unauthorized access and provide better UX than handling permissions at the component level.


My Production Setup Checklist

  1. Install with type definitions:

    npm install react-router-dom
    npm install --save-dev @types/react-router-dom
    
  2. Configure your router at the app root:

    // App.jsx - Router setup
    import { BrowserRouter } from 'react-router-dom';
    
    function App() {
      return (
        <BrowserRouter>
          <AppRoutes />
        </BrowserRouter>
      );
    }
    
  3. Set up your route structure:

    // routes/AppRoutes.jsx - Route structure
    import { Routes, Route, Navigate } from 'react-router-dom';
    
    function AppRoutes() {
      return (
        <Routes>
          <Route path="/" element={<Layout />}>
            <Route index element={<Home />} />
            <Route path="about" element={<About />} />
            <Route path="contact" element={<Contact />} />
            <Route path="*" element={<Navigate to="/" replace />} />
          </Route>
        </Routes>
      );
    }
    
  4. Use Link components for navigation:

    // Navigation components
    import { Link, NavLink } from 'react-router-dom';
    
    // Basic navigation
    <Link to="/about">About Us</Link>
    
    // Navigation with active state styling
    <NavLink 
      to="/dashboard" 
      className={({ isActive }) => isActive ? 'active' : ''}
    >
      Dashboard
    </NavLink>
    

React Router Decision Matrix

Scenario Recommended Router Key Considerations
Production web app BrowserRouter Requires server configuration
GitHub Pages deployment HashRouter Limited SEO, but works everywhere
Unit testing MemoryRouter Full control over navigation state
Mobile app (React Native) MemoryRouter No browser URL bar to sync
Server-side rendering StaticRouter Different setup for SSR frameworks

Questions My Team Asks About Routing

1. "How do I handle authentication redirects properly?"

Store the attempted URL in location state, then redirect after login. Use replace: true to prevent back-button issues. I build this into a custom hook for consistency.

2. "Should I use BrowserRouter for all web apps?"

Yes, unless you can't configure your server. The clean URLs are worth the setup effort for professional applications.

3. "How do I prevent users from accessing routes they shouldn't?"

Wrap routes in protection components that check permissions before rendering. Always validate on the server too - client-side protection is just UX.

4. "Can I use React Router with state management libraries?"

Absolutely. I often sync router state with Redux or Zustand for complex apps. The URL becomes part of your application state.

5. "How do I handle 404 pages and error boundaries?"

Use a catch-all route (path="*") at the end of your Routes. Combine with error boundaries for robust error handling throughout your app.

❤️ At Learn Virendana, we love creating high-quality React tutorials that simplify complex concepts and deliver a practical, real-world React learning experience for developers