DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 12: React Router

React Router

Introduction to React Router

React is primarily designed for building single-page applications (SPAs) where page transitions occur without full page reloads. React Router is the standard routing library for React that enables navigation among views in a React application, allowing developers to build SPAs with multiple views that are bookmarkable and shareable via URLs.

What You’ll Learn in This Chapter

  • Core concepts of React Router
  • Setting up routes in a React application
  • Route parameters and query strings
  • Nested routing
  • Protected routes
  • Code splitting with React Router
  • Navigation and redirects
  • Best practices for organizing routes

Installing React Router

To get started with React Router, you need to install it via npm or yarn:

# Using npm
npm install react-router-dom

# Using yarn
yarn add react-router-dom

For React Router v6 (the latest major version as of this writing), we’ll be using react-router-dom, which is specifically designed for web applications.

Core Components of React Router

React Router provides several key components that you’ll use frequently:

1. BrowserRouter

BrowserRouter uses the HTML5 history API to keep your UI in sync with the URL. It’s typically used as the root routing component:

import { BrowserRouter } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      {/* The rest of your app goes here */}
    </BrowserRouter>
  );
}

2. Routes and Route

The Routes component (which replaced Switch in v6) wraps multiple Route components and renders the first one that matches the current URL:

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

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

3. Link and NavLink

Link creates navigation links that update the URL without reloading the page:

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

function Navigation() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/dashboard">Dashboard</Link>
    </nav>
  );
}

NavLink is a special version of Link that knows whether it’s “active” or not:

import { NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <NavLink
        to="/"
        className={({ isActive }) => isActive ? "active-link" : ""}
      >
        Home
      </NavLink>
      <NavLink
        to="/about"
        className={({ isActive }) => isActive ? "active-link" : ""}
      >
        About
      </NavLink>
    </nav>
  );
}

4. Navigate

The Navigate component allows for programmatic navigation:

import { Navigate } from 'react-router-dom';

function RedirectComponent() {
  return <Navigate to="/new-page" replace />;
}

URL Parameters

React Router makes it easy to create dynamic routes with parameters:

import { BrowserRouter, Routes, Route, useParams } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/users" element={<UserList />} />
        <Route path="/users/:userId" element={<UserProfile />} />
      </Routes>
    </BrowserRouter>
  );
}

function UserProfile() {
  const { userId } = useParams();
 
  return (
    <div>
      <h2>User Profile</h2>
      <p>User ID: {userId}</p>
      {/* Fetch and display user data based on userId */}
    </div>
  );
}

Nested Routes

React Router v6 makes nested routing more intuitive:

import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={<Dashboard />} />
          <Route path="products" element={<Products />}>
            <Route index element={<ProductList />} />
            <Route path=":productId" element={<ProductDetail />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Layout() {
  return (
    <div>
      <header>
        <Navigation />
      </header>
      <main>
        <Outlet /> {/* Child routes will render here */}
      </main>
      <footer>(c) 2025 My React App</footer>
    </div>
  );
}

function Products() {
  return (
    <div>
      <h2>Products</h2>
      <Outlet /> {/* Nested product routes render here */}
    </div>
  );
}

The Outlet component acts as a placeholder where child routes will be rendered.

Query Parameters

React Router provides a hook to access query parameters:

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

function SearchResults() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get('q') || '';
  const sortBy = searchParams.get('sort') || 'relevance';
 
  // Change the query params programmatically
  const changeSorting = (newSortValue) => {
    setSearchParams({ q: query, sort: newSortValue });
  };
 
  return (
    <div>
      <h2>Search Results for: {query}</h2>
      <div>
        <button onClick={() => changeSorting('price')}>Sort by Price</button>
        <button onClick={() => changeSorting('name')}>Sort by Name</button>
      </div>
      {/* Display search results */}
    </div>
  );
}

Navigation Hooks

React Router provides hooks for programmatic navigation:

javascript

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

function LoginForm() {
  const navigate = useNavigate();
 
  const handleSubmit = async (e) => {
    e.preventDefault();
    // Handle login logic
    const success = await attemptLogin();
   
    if (success) {
      navigate('/dashboard');
    }
  };
 
  return (
    <form onSubmit={handleSubmit}>
      {/* Login form fields */}
      <button type="submit">Login</button>
    </form>
  );
}

Protected Routes

A common pattern is to protect certain routes based on authentication status:

import { Navigate, Outlet } from 'react-router-dom';

function ProtectedRoute({ isAuthenticated }) {
  if (!isAuthenticated) {
    // Redirect to login if not authenticated
    return <Navigate to="/login" replace />;
  }
 
  // Render child routes if authenticated
  return <Outlet />;
}

function App() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
 
  return (
    <BrowserRouter>
      <AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated }}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
         
          {/* Protected routes */}
          <Route element={<ProtectedRoute isAuthenticated={isAuthenticated} />}>
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/settings" element={<Settings />} />
            <Route path="/profile" element={<Profile />} />
          </Route>
         
          <Route path="*" element={<NotFound />} />
        </Routes>
      </AuthContext.Provider>
    </BrowserRouter>
  );
}

Route-Based Code Splitting

To improve performance, you can load components only when needed using React’s lazy loading with React Router:

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';

// Lazy load components
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Location and History Hooks

React Router provides hooks to access current location and navigate through history:

import { useLocation, useNavigate } from 'react-router-dom';

function LocationAwareComponent() {
  const location = useLocation();
  const navigate = useNavigate();
 
  return (
    <div>
      <p>Current pathname: {location.pathname}</p>
      <button onClick={() => navigate(-1)}>Go Back</button>
      <button onClick={() => navigate(1)}>Go Forward</button>
    </div>
  );
}

Custom Routes Organization

For larger applications, you might want to organize routes in a more structured way:

// routes.js
import { lazy } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const UserList = lazy(() => import('./pages/UserList'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const NotFound = lazy(() => import('./pages/NotFound'));

export const routes = [
  {
    path: '/',
    element: Home,
    exact: true,
  },
  {
    path: '/about',
    element: About,
  },
  {
    path: '/dashboard',
    element: Dashboard,
    protected: true,
  },
  {
    path: '/users',
    element: UserList,
    protected: true,
  },
  {
    path: '/users/:userId',
    element: UserProfile,
    protected: true,
  },
  {
    path: '*',
    element: NotFound,
  },
];
// App.js
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Suspense } from 'react';import { routes } from './routes';

function App() {  
const isAuthenticated = useAuth(); 
// Your authentication hook    
return (    
<BrowserRouter>      
  <Suspense fallback={<div>Loading...</div>}>
    <Routes>
      {
        routes.map((route) => {            
          const Element = route.element;
          // Handle protected routes            
          if (route.protected && !isAuthenticated) {              
            return (
              <Route key={route.path} path={route.path} element={<Navigate to="/login" replace />} />
            );
          }
          return (
            <Route  key={route.path}  path={route.path}  element={<Element />  exact={route.exact} />            
          );
          }
          )
          }        
    </Routes>
  </Suspense>
</BrowserRouter>  
);
}

Complete Example: Building a Multi-Page Application

Let’s put everything together to build a simple multi-page application with React Router:

// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import About from './pages/About';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import Dashboard from './pages/Dashboard';
import NotFound from './pages/NotFound';
import ProtectedRoute from './components/ProtectedRoute';
import { AuthProvider, useAuth } from './contexts/AuthContext';

function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Layout />}>
            {/* Public routes */}
            <Route index element={<Home />} />
            <Route path="about" element={<About />} />
            <Route path="products" element={<Products />} />
            <Route path="products/:productId" element={<ProductDetail />} />
           
            {/* Protected routes */}
            <Route element={<ProtectedRoute />}>
              <Route path="dashboard" element={<Dashboard />} />
            </Route>
           
            {/* Catch-all route */}
            <Route path="*" element={<NotFound />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
}

export default App;

// components/Layout.js
import { Outlet } from 'react-router-dom';
import Navigation from './Navigation';

function Layout() {
  return (
    <div>
      <header>
        <Navigation />
      </header>
      <main>
        <Outlet />
      </main>
      <footer>
        <p>(c) 2025 My React Router App</p>
      </footer>
    </div>
  );
}

export default Layout;

// components/Navigation.js
import { NavLink } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function Navigation() {
  const { isAuthenticated, logout } = useAuth();
 
  return (
    <nav>
      <NavLink
        to="/"
        className={({ isActive }) => isActive ? "active" : ""}
      >
        Home
      </NavLink>
      <NavLink
        to="/about"
        className={({ isActive }) => isActive ? "active" : ""}
      >
        About
      </NavLink>
      <NavLink
        to="/products"
        className={({ isActive }) => isActive ? "active" : ""}
      >
        Products
      </NavLink>
     
      {isAuthenticated ? (
        <>
          <NavLink
            to="/dashboard"
            className={({ isActive }) => isActive ? "active" : ""}
          >
            Dashboard
          </NavLink>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <NavLink
          to="/login"
          className={({ isActive }) => isActive ? "active" : ""}
        >
          Login
        </NavLink>
      )}
    </nav>
  );
}

export default Navigation;

// components/ProtectedRoute.js
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function ProtectedRoute() {
  const { isAuthenticated } = useAuth();
 
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
 
  return <Outlet />;
}

export default ProtectedRoute;

// contexts/AuthContext.js
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
 
  const login = () => setIsAuthenticated(true);
  const logout = () => setIsAuthenticated(false);
 
  return (
    <AuthContext.Provider value={{ isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

Common Issues and Solutions

1. Handling 404 Pages

React Router can handle unknown routes with a catch-all route:

<Route path="*" element={<NotFound />} />

2. Scroll Restoration

When navigating between pages, the scroll position doesn’t automatically reset. You can create a component to handle this:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

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

// Use it in your App
function App() {
  return (
    <BrowserRouter>
      <ScrollToTop />
      {/* rest of your app */}
    </BrowserRouter>
  );
}

3. Server Configuration for SPAs

For SPAs to work correctly, the server needs to be configured to redirect all requests to index.html. Here are some common server configurations:

For Apache (.htaccess):

RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

For Nginx:

location / {
  try_files $uri $uri/ /index.html;
}

Best Practices for React Router

  1. Always use the latest version: Keep up with the latest React Router updates for best performance and features.
  2. Organize routes efficiently: For larger applications, consider organizing routes in separate files.
  3. Implement code splitting: Use React’s lazy loading to improve initial load time.
  4. Handle loading states: Show loading indicators when routes are loading.
  5. Set up proper error boundaries: Handle errors gracefully in your routing.
  6. Use the ‘exact’ prop when needed: In React Router v5, use exact to match paths exactly. In v6, the exact behavior is default.
  7. Follow the DRY principle: Don’t repeat route patterns; abstract them into configuration objects.
  8. Provide meaningful 404 pages: Always have a catch-all route for unknown paths.
  9. Manage browser history properly: Be cautious with history manipulation to avoid breaking the back button.
  10. Test your routes: Write tests for your routing logic to ensure everything works as expected.

Conclusion

React Router provides a powerful and flexible solution for handling navigation in React applications. By understanding its core concepts and components, you can create complex routing patterns that enhance the user experience while maintaining clean, maintainable code.

In this chapter, we’ve covered:

  • Basic route setup and configuration
  • Dynamic routes with parameters
  • Nested routing
  • Protected routes
  • Programmatic navigation
  • Code splitting with routes
  • Best practices for React Router

In the next chapter, we’ll explore optimization and performance techniques to make your React applications faster and more efficient.

Practice Exercise

Create a simple React application with the following routes:

  1. A home page (/)
  2. An about page (/about)
  3. A product listing page (/products)
  4. A product detail page (/products/:productId)
  5. A protected dashboard page (/dashboard) that requires authentication
  6. A 404 page for unknown routes

Include proper navigation between these pages and implement protected routes for the dashboard.

Further Resources

Scroll to Top