Back to posts

Routing in React: A Complete Guide to React Router v6

Erik Nguyen / December 21, 2024

Routing in React: A Complete Guide to React Router v6

Navigation is a fundamental aspect of any web application. React Router v6 provides a powerful and intuitive way to handle routing in React applications. Let's explore how to create seamless navigation experiences for your users, starting from the basics and moving to advanced concepts.

Understanding the Fundamentals

React Router v6 brought significant changes in how we handle routing compared to previous versions. The new API is more declarative and easier to understand, while providing powerful features for complex routing scenarios.

First, let's set up React Router in our application:

# Install React Router
npm install react-router-dom@6

Basic Route Setup

Let's start with a simple example that demonstrates the basic concepts:

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

// Our page components
import Home from './pages/Home';
import About from './pages/About';
import Products from './pages/Products';
import NotFound from './pages/NotFound';

function App() {
  return (
    <BrowserRouter>
      {/* Navigation menu */}
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/products">Products</Link>
      </nav>

      {/* Route definitions */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/products" element={<Products />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

In this example, we've created a basic navigation structure. The BrowserRouter component provides the routing context, while Routes and Route components define our application's routing rules. The Link component creates navigation links that won't trigger a full page reload.

Nested Routes and Layouts

React Router v6 makes it easy to create nested routes and shared layouts. This is particularly useful for creating complex navigation structures:

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

// Layout component with shared UI elements
function DashboardLayout() {
  return (
    <div className="dashboard-layout">
      <nav className="dashboard-nav">
        <Link to="profile">Profile</Link>
        <Link to="settings">Settings</Link>
        <Link to="notifications">Notifications</Link>
      </nav>

      {/* Outlet renders the child route components */}
      <main className="dashboard-content">
        <Outlet />
      </main>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        {/* Nested routes within dashboard */}
        <Route path="dashboard" element={<DashboardLayout />}>
          <Route index element={<DashboardHome />} />
          <Route path="profile" element={<Profile />} />
          <Route path="settings" element={<Settings />} />
          <Route path="notifications" element={<Notifications />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

The Outlet component is a key concept in React Router v6. It acts as a placeholder where child routes will be rendered, allowing you to create consistent layouts with shared UI elements.

Dynamic Routes and Parameters

Real-world applications often need to handle dynamic routes with parameters. Here's how to implement them:

// Route definition with URL parameters
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="products" element={<Products />}>
          <Route path=":productId" element={<ProductDetails />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

// Component using URL parameters
function ProductDetails() {
  const { productId } = useParams();
  const [product, setProduct] = useState(null);

  useEffect(() => {
    // Fetch product details using the productId
    async function fetchProduct() {
      try {
        const data = await fetchProductDetails(productId);
        setProduct(data);
      } catch (error) {
        console.error('Failed to fetch product:', error);
      }
    }

    fetchProduct();
  }, [productId]);

  if (!product) return <div>Loading...</div>;

  return (
    <div className="product-details">
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <price>${product.price}</price>
    </div>
  );
}

Protected Routes and Authentication

A common requirement is protecting certain routes based on authentication status. Here's how to implement protected routes:

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

// Auth context for managing authentication state
const AuthContext = createContext(null);

function ProtectedRoute({ children }) {
  const { user } = useContext(AuthContext);
  const location = useLocation();

  if (!user) {
    // Redirect to login while saving the attempted URL
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="login" element={<Login />} />

          {/* Protected dashboard routes */}
          <Route
            path="dashboard"
            element={
              <ProtectedRoute>
                <DashboardLayout />
              </ProtectedRoute>
            }
          >
            <Route index element={<DashboardHome />} />
            <Route path="profile" element={<Profile />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
}

// Login component with redirect after authentication
function Login() {
  const { login } = useContext(AuthContext);
  const location = useLocation();
  const navigate = useNavigate();

  const handleLogin = async (credentials) => {
    try {
      await login(credentials);
      // Redirect to the page they tried to visit or dashboard
      const destination = location.state?.from?.pathname || '/dashboard';
      navigate(destination, { replace: true });
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  return (
    <form onSubmit={handleLogin}>
      {/* Login form fields */}
    </form>
  );
}

Advanced Navigation Features

React Router v6 provides several advanced features for complex navigation scenarios:

1. Programmatic Navigation

function ProductCard({ product }) {
  const navigate = useNavigate();

  const handleClick = () => {
    // Navigate programmatically with state
    navigate(`/products/${product.id}`, {
      state: { productData: product }
    });
  };

  return (
    <div onClick={handleClick}>
      <h3>{product.name}</h3>
      <p>{product.shortDescription}</p>
    </div>
  );
}

2. Route Loaders and Actions

React Router v6.4+ introduced data loading and mutation capabilities:

// Route configuration with loader and action
const router = createBrowserRouter([
  {
    path: "/products",
    element: <Products />,
    loader: async () => {
      const products = await fetchProducts();
      return { products };
    },
    action: async ({ request }) => {
      const formData = await request.formData();
      return createProduct(Object.fromEntries(formData));
    }
  }
]);

// Component using loader data
function Products() {
  const { products } = useLoaderData();
  const { state } = useNavigation();

  if (state === 'loading') return <LoadingSpinner />;

  return (
    <div className="products-grid">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

3. Error Boundaries

Handle routing errors gracefully:

function ErrorBoundary() {
  const error = useRouteError();

  return (
    <div className="error-container">
      <h1>Oops! Something went wrong</h1>
      <p>{error.message}</p>
      <Link to="/">Return to Home</Link>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorBoundary />,
    children: [
      // ... other routes
    ]
  }
]);

Best Practices and Performance Tips

  1. Lazy Loading Routes
import { lazy, Suspense } from 'react';

// Lazy load route components
const ProductDetails = lazy(() => import('./pages/ProductDetails'));

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route
          path="products/:id"
          element={
            <Suspense fallback={<LoadingSpinner />}>
              <ProductDetails />
            </Suspense>
          }
        />
      </Routes>
    </BrowserRouter>
  );
}
  1. Scroll Management
function ScrollToTop() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
}

function App() {
  return (
    <BrowserRouter>
      <ScrollToTop />
      <Routes>{/* ... */}</Routes>
    </BrowserRouter>
  );
}

Common Patterns and Solutions

1. Breadcrumb Navigation

function Breadcrumbs() {
  const location = useLocation();
  const paths = location.pathname.split('/')
    .filter(Boolean);

  return (
    <nav aria-label="breadcrumb">
      <ol className="breadcrumb">
        <li>
          <Link to="/">Home</Link>
        </li>
        {paths.map((path, index) => {
          const routeTo = `/${paths.slice(0, index + 1).join('/')}`;
          const isLast = index === paths.length - 1;

          return (
            <li key={routeTo}>
              {isLast ? (
                <span>{path}</span>
              ) : (
                <Link to={routeTo}>{path}</Link>
              )}
            </li>
          );
        })}
      </ol>
    </nav>
  );
}

2. Modal Routes

function ProductsLayout() {
  return (
    <div className="products-page">
      <h1>Products</h1>
      <div className="products-grid">
        <Outlet />
      </div>

      {/* Modal route outlet */}
      <div className="modal-container">
        <Outlet context="modal" />
      </div>
    </div>
  );
}

// Route configuration
<Route path="products" element={<ProductsLayout />}>
  <Route index element={<ProductsList />} />
  <Route
    path=":id"
    element={<ProductModal />}
  />
</Route>

Conclusion

React Router v6 provides a powerful and flexible routing solution for React applications. By understanding its core concepts and features, you can create intuitive navigation experiences that scale with your application's complexity.

Remember these key points:

  • Use nested routes for complex layouts
  • Implement protected routes for authentication
  • Leverage dynamic parameters for flexible routing
  • Consider code splitting for better performance
  • Handle errors gracefully with error boundaries

Happy routing! 🚀