| |

React 59 ⚛️ Defining Route Elements and Outlets

⏱️ Estimated read: 19 min | |

A route has two jobs: match a URL, and produce an element. React Router separates these. The matching is done by the route configuration, and the rendering is done by the element prop on each route. The element is a React element, not a component reference, and it is rendered when the route matches. The <Outlet> component is the placeholder for the child route’s element, and it is what makes nested layouts work. A parent route renders a shared header, a sidebar, and an <Outlet>, and the child route renders into the outlet. The two together — the element prop and the <Outlet> component — are the mechanism that composes a route tree into a rendered page.

Key point: The element prop takes a React element (<Home />), not a component reference (Home). The <Outlet> component renders the matched child route. A parent route without an <Outlet> renders its own element but nothing from its children, and the child route’s element is discarded.


Why route elements and outlets exist

The composition problem. An application’s pages share a header, a navigation bar, a sidebar, and a footer. Rendering these on every page duplicates the markup and the state. A route layout renders them once, and the <Outlet> swaps only the content area.

The matching problem. React Router needs to know which element to render for a given URL. The route configuration maps paths to elements. The element prop is the destination of the mapping.

The nesting problem. A URL like /teams/firebirds corresponds to a hierarchy: a layout, a teams list, and a specific team. The route tree mirrors this hierarchy, and the <Outlet> at each level renders the next level down.

The state-preservation problem. When the user navigates from /teams/firebirds to /teams/dolphins, the layout, header, and sidebar should not remount. The <Outlet> swaps only the content, so the surrounding components keep their state.

The data-loading problem. A parent route can load data that its children need. The loader runs at the parent level, and the children read the data from the outlet context. The element and the outlet are the two halves of that pattern.

The index-route problem. A parent route needs a default child that renders when the parent’s URL matches exactly. The index route fills this role, and it is rendered into the parent’s outlet like any other child.

The error-boundary problem. A route can define an errorElement that renders when the route’s loader or element throws. The element and the error element are siblings in the route configuration.


a. The element prop

The element prop on a <Route> specifies the React element to render when the route matches. The value is a React element, not a component reference.

The basic element

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

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

The <Home /> is a React element. React Router renders it when the URL is /. The element is created during the parent’s render, and React Router stores it and renders it when the route matches.

The component reference mistake

Passing a component reference instead of an element does not work.

// ❌ WRONG: component reference
<Route path="/" Component={Home} />

// ✅ CORRECT: React element
<Route path="/" element={<Home />} />

The Component prop exists in React Router v6.4 and later as an alternative that takes a component reference and renders it with the route context. But the standard form is element, and it takes an element.

The element with props

The element can receive props from the route configuration.

<Route path="/user" element={<User role="admin" />} />

The User component renders with role equal to "admin". The props are static, set at configuration time.

The element with route params

The element receives the route’s params through the useParams hook, not through props.

<Route path="teams/:teamId" element={<Team />} />
function Team() {
  const { teamId } = useParams();
  return <h1>Team {teamId}</h1>;
}

The teamId is captured from the URL and is available inside the element.

The element with layout

The element can be a layout component that renders an <Outlet>.

<Route path="/" element={<Layout />}>
  <Route index element={<Home />} />
  <Route path="about" element={<About />} />
</Route>

The Layout element is rendered when the URL starts with /. The <Outlet> inside Layout renders the child route’s element.

The element and the route context

Every element rendered by a route has access to the route context: useParams, useLocation, useNavigate, useOutlet, and the rest. The context is provided by the router, and it is available to any descendant of the route.

The element lifecycle

The element is created during the parent’s render, and React Router renders it when the route matches. When the route changes, React Router renders a different element. React reconciles the tree, and components that are common to both routes stay mounted.


b. The Outlet component

The <Outlet> component renders the element of the matched child route. It is the placeholder that makes nested layouts work.

The basic outlet

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

function Layout() {
  return (
    <div>
      <header>Application Header</header>
      <main>
        <Outlet />
      </main>
      <footer>Application Footer</footer>
    </div>
  );
}

The <Outlet> is where the child route’s element appears. The header and footer stay mounted across navigations.

The nested outlets

An outlet can render a child that has its own outlet, producing a hierarchy of layouts.

<Routes>
  <Route path="/" element={<RootLayout />}>
    <Route path="teams" element={<TeamsLayout />}>
      <Route index element={<TeamsList />} />
      <Route path=":teamId" element={<TeamDetail />} />
    </Route>
  </Route>
</Routes>

The RootLayout renders the global header and an outlet. The TeamsLayout renders the teams-specific navigation and an outlet. The TeamDetail renders the team’s details.

The outlet with context

The useOutletContext hook passes data from the parent route to the child route.

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

function Dashboard() {
  const [user, setUser] = useState(null);
  return <Outlet context={{ user, setUser }} />;
}

function Profile() {
  const { user, setUser } = useOutletContext();
  return <h1>{user?.name}</h1>;
}

The context is created in the parent’s render and is available in the child through useOutletContext. This is the pattern for sharing data between a layout and its children without prop drilling.

The useOutlet hook

The useOutlet hook returns the child route’s element without rendering it. It is used when the parent needs to inspect or wrap the element.

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

function Layout() {
  const outlet = useOutlet();
  return (
    <div>
      {outlet ? outlet : <p>No child route</p>}
    </div>
  );
}

The hook returns null when there is no matched child route. This is useful for rendering a fallback in the layout when the URL matches the parent but not a child.

The outlet and the index route

An index route renders into the parent’s outlet when the parent’s URL matches exactly.

<Route path="/" element={<Layout />}>
  <Route index element={<Home />} />
  <Route path="about" element={<About />} />
</Route>

The URL / renders Layout with Home in the outlet. The URL /about renders Layout with About in the outlet.

The outlet and the layout state

Because the layout is not remounted when the child route changes, its state is preserved. A scroll position, an open sidebar, or a form draft in the layout survives navigation.

The outlet and the error element

If a child route’s element throws, the nearest errorElement renders instead of the child. The outlet’s parent is not affected.


c. Combining elements and outlets

The element and the outlet are two halves of the same mechanism. The element defines what each route renders; the outlet defines where the child route’s element appears.

The route tree

The route configuration is a tree, and the rendered output is a tree. The two mirror each other.

Route tree:
  /                  → <Layout>
    index            → <Home>
    about            → <About>
    teams            → <TeamsLayout>
      index          → <TeamsList>
      :teamId        → <TeamDetail>

Rendered tree for /teams/firebirds:
  <Layout>
    <header />
    <main>
      <TeamsLayout>
        <nav />
        <TeamDetail teamId="firebirds" />
      </TeamsLayout>
    </main>
    <footer />
  </Layout>

Each route’s element renders, and the <Outlet> in each element is replaced by the next level’s element.

The layout persistence

The layout’s element stays mounted when the child route changes. Only the outlet’s content is swapped.

// Navigating from /teams/firebirds to /teams/dolphins:
// <Layout> stays mounted.
// <TeamsLayout> stays mounted.
// <TeamDetail> remounts with a new teamId.

This is the mechanism that preserves scroll position and layout state across navigations.

The conditional outlet

A layout can render different UI depending on whether the outlet is present.

function Layout() {
  const outlet = useOutlet();
  return (
    <div>
      <Sidebar />
      <main>
        {outlet ?? <EmptyState />}
      </main>
    </div>
  );
}

The useOutlet hook returns null when there is no child match, and the layout can render a fallback.

The outlet and the loader

In the data router API, a parent route’s loader runs before the child route’s element renders. The parent’s element can access the loader data, and the child’s element can access it through the outlet context.

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    loader: () => fetchUser(),
    children: [
      { index: true, element: <Home /> },
    ],
  },
]);

The Layout element reads the user via useLoaderData, and the Home element can read it via useOutletContext if the layout passes it.

The outlet and the error element

Each route can define an errorElement. When the route’s element throws, the nearest errorElement renders in place of the route’s element. The parent’s outlet is replaced by the error element.

<Route path="/teams" element={<TeamsLayout />} errorElement={<TeamsError />}>
  <Route path=":teamId" element={<TeamDetail />} />
</Route>

If TeamDetail throws, the TeamsError element renders inside the TeamsLayout‘s outlet.

The outlet and the route hierarchy

The outlet is the mechanism that connects the route tree to the rendered tree. Every level in the route tree has an element, and every level except the leaves has an outlet. The element renders the layout, and the outlet renders the next level down.


Complete Example Session

// ============================================
// PART 1: THE ROUTE CONFIGURATION
// ============================================

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import RootLayout from './layouts/RootLayout';
import TeamsLayout from './layouts/TeamsLayout';
import Home from './pages/Home';
import About from './pages/About';
import TeamsList from './pages/TeamsList';
import TeamDetail from './pages/TeamDetail';
import NotFound from './pages/NotFound';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'about', element: <About /> },
      {
        path: 'teams',
        element: <TeamsLayout />,
        children: [
          { index: true, element: <TeamsList /> },
          { path: ':teamId', element: <TeamDetail /> },
        ],
      },
      { path: '*', element: <NotFound /> },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

// ============================================
// PART 2: THE ROOT LAYOUT
// ============================================

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

function RootLayout() {
  return (
    <div className="app">
      <header>
        <nav>
          <NavLink to="/">Home</NavLink>
          <NavLink to="/about">About</NavLink>
          <NavLink to="/teams">Teams</NavLink>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>© 2026</footer>
    </div>
  );
}

// ============================================
// PART 3: THE TEAMS LAYOUT
// ============================================

function TeamsLayout() {
  return (
    <div className="teams">
      <aside>
        <h2>Teams</h2>
        <NavLink to="/teams/firebirds">Firebirds</NavLink>
        <NavLink to="/teams/dolphins">Dolphins</NavLink>
      </aside>
      <section>
        <Outlet />
      </section>
    </div>
  );
}

// ============================================
// PART 4: THE INDEX ROUTE
// ============================================

function TeamsList() {
  return (
    <div>
      <h1>All Teams</h1>
      <p>Select a team from the sidebar.</p>
    </div>
  );
}

// The index route renders at /teams when no child matches.

// ============================================
// PART 5: THE DYNAMIC ROUTE
// ============================================

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

function TeamDetail() {
  const { teamId } = useParams();
  return (
    <div>
      <h1>Team: {teamId}</h1>
      <p>Details about {teamId}.</p>
    </div>
  );
}

// The URL /teams/firebirds renders TeamDetail with teamId="firebirds".

// ============================================
// PART 6: THE OUTLET CONTEXT
// ============================================

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

function Dashboard() {
  const [user] = useState({ name: 'Alice' });
  return <Outlet context={{ user }} />;
}

function Profile() {
  const { user } = useOutletContext();
  return <h1>Welcome, {user.name}</h1>;
}

// The parent passes data to the child through the outlet context.

// ============================================
// PART 7: THE useOutlet HOOK
// ============================================

function ConditionalLayout() {
  const outlet = useOutlet();
  return (
    <div>
      <Sidebar />
      <main>
        {outlet ?? <EmptyState />}
      </main>
    </div>
  );
}

// The hook returns null when there is no child match.

// ============================================
// PART 8: THE ERROR ELEMENT
// ============================================

const routerWithError = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        path: 'teams',
        element: <TeamsLayout />,
        errorElement: <TeamsError />,
        children: [
          { index: true, element: <TeamsList /> },
          { path: ':teamId', element: <TeamDetail /> },
        ],
      },
    ],
  },
]);

function TeamsError() {
  const error = useRouteError();
  return <p>Failed to load teams: {error.message}</p>;
}

// The error element renders inside the parent's outlet.

// ============================================
// PART 9: THE LAYOUT PERSISTENCE
// ============================================

// Navigating from /teams/firebirds to /teams/dolphins:
// <RootLayout> stays mounted.
// <TeamsLayout> stays mounted.
// <TeamDetail> remounts with a new teamId.
// The sidebar scroll position is preserved.

// ============================================
// PART 10: THE COMPLETE PICTURE
// ============================================

// The route tree:
//   /            → RootLayout (outlet)
//     index      → Home
//     about      → About
//     teams      → TeamsLayout (outlet)
//       index    → TeamsList
//       :teamId  → TeamDetail
//     *          → NotFound
//
// The rendered tree for /teams/firebirds:
//   <RootLayout>
//     <header />
//     <main>
//       <TeamsLayout>
//         <aside />
//         <TeamDetail teamId="firebirds" />
//       </TeamsLayout>
//     </main>
//     <footer />
//   </RootLayout>

The ten parts showed the route configuration, the root layout, the teams layout, the index route, the dynamic route, the outlet context, the useOutlet hook, the error element, the layout persistence, and the complete picture.


Quick Reference

The element Prop

FormExampleCorrect?
React elementelement={<Home />}✅
Component referenceComponent={Home}Alternative
Function callelement={Home()}❌

The Outlet Component

HookPurpose
<Outlet />Render the matched child route
<Outlet context={...} />Pass data to the child
useOutlet()Get the child element or null
useOutletContext()Read the parent’s context

Route Patterns

PatternElementOutlet
Layout routeLayout componentYes
Index routePage componentNo
Leaf routePage componentNo
Catch-all404 componentNo

Nested Layouts

LevelElementOutlet Renders
Root<RootLayout />Teams or page
Teams<TeamsLayout />Team detail or list
Team<TeamDetail />Nothing

Error Handling

PropPurpose
elementThe route’s normal element
errorElementThe element rendered on error
useRouteError()Read the error in the error element

Best Practices

✅ Do This:

// Pass a React element to the element prop.
<Route path="/" element={<Home />} />  // ✅

// Use an Outlet in every layout component.
function Layout() {
  return <div><Header /><Outlet /><Footer /></div>;
}  // ✅

// Use an index route for the default child.
<Route index element={<Home />} />  // ✅

// Pass data to children via the outlet context.
<Outlet context={{ user }} />  // ✅

// Read the context with useOutletContext.
const { user } = useOutletContext();  // ✅

// Use errorElement for route-level errors.
<Route errorElement={<ErrorPage />} />  // ✅

// Keep the layout mounted across navigations.
// The layout's state is preserved.  // ✅

❌ Don’t Do This:

// Don't pass a component reference to element.
<Route path="/" element={Home} />  // ❌ Wrong type

// Don't forget the Outlet in a layout.
function Layout() { return <div><Header /><Footer /></div>; }  // ❌ Children never render

// Don't nest routes without a layout.
<Route path="/teams" element={<Teams />}>
  <Route path=":teamId" element={<Team />} />  // ❌ Teams has no Outlet
</Route>

// Don't access useOutletContext outside the outlet.
const { user } = useOutletContext();  // ❌ Only valid inside the child

// Don't put the errorElement on the wrong route.
// It should be on the route whose children may throw.  // ❌

// Don't assume the outlet is always present.
const outlet = useOutlet();
if (!outlet) return <Fallback />;  // ✅

Common Pitfalls

PitfallWhy It HappensFix
Child route not renderingMissing <Outlet>Add <Outlet> to the layout
element type errorComponent reference passedWrap in JSX
Index route not renderingMissing index propAdd index to the child route
Context is undefineduseOutletContext outside the childCall it inside the child route
Layout remounts on navigationDifferent element per routeShare the layout via nested routes
Error not caughtNo errorElementAdd it to the route
Splat route matches firstRoute orderPut the splat last

Real-World Examples

1. Basic Element

<Route path="/" element={<Home />} />

2. Layout with Outlet

function Layout() {
  return <div><Header /><Outlet /><Footer /></div>;
}

3. Index Route

<Route index element={<Home />} />

4. Nested Layout

<Route path="teams" element={<TeamsLayout />}>
  <Route index element={<TeamsList />} />
  <Route path=":teamId" element={<TeamDetail />} />
</Route>

5. Outlet Context

<Outlet context={{ user }} />

6. Read Outlet Context

const { user } = useOutletContext();

7. useOutlet Hook

const outlet = useOutlet();
return <div>{outlet ?? <Fallback />}</div>;

8. Error Element

<Route errorElement={<ErrorPage />} />

9. Read Route Error

const error = useRouteError();

10. Catch-All

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

Visual

The Route Tree and Rendered Tree

┌────────────────────────────────────────────────────────────┐
│  ROUTE TREE                  RENDERED TREE                 │
│                                                            │
│  / ─────────────▶ RootLayout                               │
│    ├─ index ─────▶ Home                                    │
│    ├─ about ─────▶ About                                   │
│    └─ teams ─────▶ TeamsLayout                             │
│         ├─ index ─▶ TeamsList                              │
│         └─ :id ───▶ TeamDetail                             │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <RootLayout>                                     │     │
│  │    <Header />                                     │     │
│  │    <Outlet>                                       │     │
│  │      <TeamsLayout>                                │     │
│  │        <Sidebar />                                │     │
│  │        <Outlet>                                   │     │
│  │          <TeamDetail teamId="firebirds" />        │     │
│  │        </Outlet>                                  │     │
│  │      </TeamsLayout>                               │     │
│  │    </Outlet>                                      │     │
│  │    <Footer />                                     │     │
│  │  </RootLayout>                                    │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Each Outlet is filled by the next level of the tree.      │
└────────────────────────────────────────────────────────────┘

The element Prop

┌────────────────────────────────────────────────────────────┐
│  element vs Component                                      │
│                                                            │
│  ✅ element={<Home />}                                      │
│  ┌──────────────────────────────────────────────────┐     │
│  │  A React element.                                 │     │
│  │  Created during the parent's render.              │     │
│  │  The standard form.                               │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  ⚠️  Component={Home}                                       │
│  ┌──────────────────────────────────────────────────┐     │
│  │  A component reference.                           │     │
│  │  React Router renders it with route context.      │     │
│  │  Available in v6.4+.                              │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  ❌ element={Home}                                          │
│  ┌──────────────────────────────────────────────────┐     │
│  │  A component reference passed to element.         │     │
│  │  React Router tries to render a function as a     │     │
│  │  React child. This fails.                         │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

Nested Outlets

┌────────────────────────────────────────────────────────────┐
│  NESTED OUTLETS                                            │
│                                                            │
│  <Routes>                                                  │
│    <Route path="/" element={<RootLayout />}>              │
│      <Route path="teams" element={<TeamsLayout />}>       │
│        <Route index element={<TeamsList />} />            │
│        <Route path=":teamId" element={<TeamDetail />} />  │
│      </Route>                                             │
│    </Route>                                               │
│  </Routes>                                                │
│                                                            │
│  RootLayout                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <header>...</header>                             │     │
│  │  <Outlet>  ← TeamsLayout renders here             │     │
│  │    ┌──────────────────────────────────────────┐   │     │
│  │    │  <aside>...</aside>                       │   │     │
│  │    │  <Outlet>  ← TeamDetail renders here      │   │     │
│  │    │    ┌──────────────────────────────┐       │   │     │
│  │    │    │  <h1>Team: firebirds</h1>     │       │   │     │
│  │    │    └──────────────────────────────┘       │   │     │
│  │    └──────────────────────────────────────────┘   │     │
│  │  </Outlet>                                        │     │
│  │  <footer>...</footer>                             │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Each Outlet is a slot for the next level down.            │
└────────────────────────────────────────────────────────────┘

Layout Persistence

┌────────────────────────────────────────────────────────────┐
│  LAYOUT PERSISTENCE ACROSS NAVIGATION                      │
│                                                            │
│  /teams/firebirds → /teams/dolphins                        │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <RootLayout>     ← Stays mounted                  │     │
│  │    <Header />                                      │     │
│  │    <TeamsLayout>  ← Stays mounted                  │     │
│  │      <Sidebar />  ← Scroll position preserved      │     │
│  │      <TeamDetail teamId="dolphins" />              │     │
│  │        ← Remounts with new params                  │     │
│  │    </TeamsLayout>                                  │     │
│  │    <Footer />                                      │     │
│  │  </RootLayout>                                     │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Only the leaf route's element remounts.                   │
│  The layouts keep their state.                             │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
element propThe React element a route renders
Component propAlternative that takes a component reference
<Outlet>Placeholder for the child route’s element
useOutlet()Hook that returns the child element or null
useOutletContext()Hook that reads the parent’s context
Index routeDefault child rendered at the parent’s URL
Layout routeParent route with an outlet
Leaf routeRoute with no children
errorElementElement rendered when the route throws
useRouteError()Hook to read the error in the error element

Key takeaways:

  • The element prop takes a React element, not a component reference. The correct form is element={<Home />}. The Component={Home} form is an alternative available in v6.4+, but the element form is the standard.
  • The <Outlet> renders the matched child route. A layout component renders its shared markup and an <Outlet>. The child route’s element appears where the outlet is. Without an outlet, the child’s element is discarded.
  • Nested routes produce nested outlets. A parent layout renders an outlet, the child layout renders another outlet, and the leaf route renders the final content. Each level fills the level above it.
  • Layouts stay mounted across navigations. The root layout and the intermediate layouts are not remounted when the leaf route changes. This preserves their state, including scroll position and form drafts.
  • The outlet context passes data from parent to child. The <Outlet context={...} /> prop makes data available to the child route, and useOutletContext() reads it. This is the pattern for sharing layout-level data without prop drilling.
  • The useOutlet() hook returns the child element or null. It is used when the layout needs to inspect the outlet or render a fallback when there is no matched child.
  • The index route renders at the parent’s URL. A child route with the index prop renders when the parent’s path matches exactly and no other child matches.
  • The errorElement renders when a route throws. Each route can define its own error element, and the nearest one catches the error. The useRouteError() hook reads the error inside the error element.

Remember: The element prop and the <Outlet> component are the two halves of React Router’s rendering mechanism. The element defines what each route renders, and the <Outlet> defines where the child route’s element appears. The route tree mirrors the URL hierarchy, and the rendered tree mirrors the route tree. Each layout renders its shared markup and an outlet, and the child route’s element fills the outlet. The layout stays mounted across navigations, so its state is preserved. The outlet context passes data from the layout to the children. The index route handles the parent’s exact URL, and the error element handles route-level failures. The result is a composed tree where each route contributes its own element, and the outlets connect them into a single rendered page.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!