Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 52

Nested Routes and Layouts with React Router Outlet

Recommended resources

Hello! Welcome back.

In our last lesson, we successfully built dynamic product detail pages using the useParams hook. This allowed us to render specific product information based on the ID in the URL, like /products/1.

You might have noticed that as our application grows, we'll want to maintain a consistent look and feel across related pages. For example, both the product listing page and the product detail page could share a common header, a sub-navigation menu, or a "Back" button. Duplicating this layout code in every component is inefficient and hard to maintain.

This is where today's topic comes in. We will learn how to create shared layouts using nested routes and the Outlet component.

This concept has a direct parallel in your Laravel experience. In Laravel, you use Blade layouts with @extends to define a master template and @yield (or @slot) to specify where child content should be injected. In React Router, a parent route component acts as the layout, and the <Outlet /> component serves the exact same purpose as @yield—it's a placeholder for the content of the child route.

By the end of this lesson, you will be able to create a shared layout for our product-related pages, keeping our code clean and organized.

1. Understanding Nested Routes and the Outlet Component

Nested routing is a powerful feature of React Router that allows you to map your component structure to your URL structure. The core idea is simple: you can define routes inside other routes.

When you do this, the parent route's component becomes a layout component. It renders the common UI elements (like a navbar, sidebar, or footer), and then uses a special component called <Outlet /> to render the component of the matched child route.

To get a clear visual and conceptual understanding, let's watch a video that walks through this process.

React Router in Depth #4 - Nested Routes & Layouts

This video from the Net Ninja channel provides an excellent explanation of nested routes and shared layouts. It demonstrates how to create a layout component and use the <Outlet /> to render the content of child routes within it.

Please watch the following sections: Introduction to Nested Routes (00:28 - 01:41): Focus on how a parent route (/help) can contain child routes (/faq, /contact). Creating the Layout Component (02:00 - 04:19): Pay close attention to how the HelpLayout component is created and where the crucial <Outlet /> component is placed. Seeing it in Action (09:55 - 10:50): This part shows the final result in the browser, demonstrating how the HelpLayout persists while the content inside the Outlet changes.

As a quick reference, the official React Router documentation defines the Outlet component clearly.

Outlet

Take a quick look at the official React Router documentation for the Outlet component. You only need to read the 'Summary' section.

Read the short 'Summary' section at the top of the page. It provides a concise definition of what the Outlet component does.


2. Practical Implementation: Creating a ProductsLayout

Now, let's apply this concept to our own application. We'll create a ProductsLayout component that will serve as a shared wrapper for both our ProductsPage (the list) and ProductDetailPage.

Step 1: Create the Layout Component

First, let's create a new folder for our layouts to keep the project organized.

  1. Create a new folder named layouts inside your src folder.
  2. Inside src/layouts, create a new file named ProductsLayout.jsx.

Now, add the following code to src/layouts/ProductsLayout.jsx. This component will contain the shared UI and the essential Outlet for child components.

// src/layouts/ProductsLayout.jsx
import React from 'react';
import { Outlet, Link } from 'react-router-dom';

const ProductsLayout = () => {
  return (
    <div className="products-layout">
      <header>
        <nav>
          {/* This link will be visible on both the list and detail pages */}
          <Link to="/">Back to Home</Link>
        </nav>
      </header>

      <main>
        {/* Child route components will be rendered here */}
        <Outlet />
      </main>
    </div>
  );
};

export default ProductsLayout;

In this component:

  • We've created a simple layout with a header and a main section.
  • The <Link to="/">Back to Home</Link> is part of our shared layout.
  • The <Outlet /> component, imported from react-router-dom, marks the spot where either ProductsPage or ProductDetailPage will be rendered.

Step 2: Update Your Route Configuration

Next, we need to refactor our routes in App.jsx to use this new layout.

Open src/App.jsx and modify the <Routes> section to nest the product-related routes.

// src/App.jsx

// ... other imports
import HomePage from './pages/HomePage';
import ProductsPage from './pages/ProductsPage';
import ProductDetailPage from './pages/ProductDetailPage';
import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage'; // Assuming you have this
import ProductsLayout from './layouts/ProductsLayout'; // 1. Import the new layout

function App() {
  return (
    <BrowserRouter>
      {/* ... your main Nav component ... */}
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />

        {/* 2. Create the parent route with the layout */}
        <Route path="/products" element={<ProductsLayout />}>
          {/* 3. The index route for the product list */}
          <Route index element={<ProductsPage />} />
          {/* 4. The nested route for a single product */}
          <Route path=":productId" element={<ProductDetailPage />} />
        </Route>
        
        {/* <Route path="*" element={<NotFoundPage />} /> */}
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Let's break down these critical changes:

  1. Import Layout: We import our new ProductsLayout component.
  2. Parent Route: We create a <Route> with path="/products" that renders our <ProductsLayout />. This route now acts as a wrapper.
  3. Index Route: The original ProductsPage route is now nested inside. We've replaced path="/products" with the index prop. An index route renders in its parent's Outlet when the URL path matches the parent's path exactly. So, when you visit /products, ProductsLayout renders, and ProductsPage appears in its Outlet.
  4. Nested Dynamic Route: The ProductDetailPage route is also nested. Its path is now just :productId. React Router understands this is relative to the parent path, so the full path it matches is /products/:productId.

Now, run your application (npm run dev) and see the result.

  • Navigate to /products. You should see the "Back to Home" link from ProductsLayout and below it, your list of products from ProductsPage.
  • Click on any product. The URL will change to /products/1 (or similar), and you'll see the product details from ProductDetailPage. Importantly, the "Back to Home" link from the layout is still there.

You have successfully created a shared layout!


Conclusion

Great job! You've learned one of the most important patterns for building scalable and maintainable React applications. By using nested routes and the <Outlet /> component, you can avoid code duplication and create logical, hierarchical layouts that mirror your application's structure.

Key Takeaways:

  • Nested routes allow you to group related pages under a common parent route.
  • The parent route's component acts as a shared layout for all its child routes.
  • The <Outlet /> component is the essential placeholder that determines where child route components are rendered within the layout.
  • The index prop on a nested <Route> specifies the default component to render when the URL matches the parent's path exactly.

Preview of the Next Lesson:

Currently, if you type a non-existent URL like /this-page-does-not-exist, you probably see a blank page. This isn't a great user experience. In the next lesson, we will address this by implementing a 'catch-all' route to display a custom "404 Not Found" page.