skip to content

In React Router v7, how does a project layout route share its project with child routes through <Outlet context>, and how far does that value reach?

level: middleimportance: should knowfreq 38%

answer

  1. a prop on the outlet
  2. a hook in the child
  3. one outlet, one level
  4. a typed wrapper hook

basics

~20 s

The layout renders <Outlet context={value} /> and child route components call useOutletContext() to read it. The value reaches the route rendered in that outlet and its components, but not grandchild routes behind another bare <Outlet />.

solid answer

~40 s

`<Outlet>` accepts a `context` prop, and `useOutletContext()` returns it inside the route element rendered in that outlet, including that element's own descendants. It is backed by a React context whose provider wraps each outlet's content, so every `<Outlet />` sets a new value: a child route that renders a bare `<Outlet />` gives its own children `undefined`. To reach grandchildren, re-pass the value at each level, or use an ordinary React context provider in the layout. The hook is untyped by default; the docs recommend the parent exports a small typed hook, such as `useProject()`, that wraps `useOutletContext<ProjectContext>()`. Use it for UI state the layout owns, like a selected tab or a collapsed panel, not as a substitute for loading each route's own data.

code

tsx · 20 lines
tsx
import { Outlet, useOutletContext } from "react-router";
import { useState } from "react";

type Panel = "open" | "closed";
type ProjectOutlet = { projectName: string; panel: Panel; setPanel: (p: Panel) => void };

export function ProjectFrame({ projectName }: { projectName: string }) {
  const [panel, setPanel] = useState<Panel>("open");
  return (
    <section>
      <h1>{projectName}</h1>
      <Outlet context={{ projectName, panel, setPanel } satisfies ProjectOutlet} />
    </section>
  );
}

// typed accessor exported by the parent route module
export function useProjectOutlet() {
  return useOutletContext<ProjectOutlet>();
}

go deeper

for a junior

Recall that a layout passes values with <Outlet context={...} /> and a child route reads them with useOutletContext().

for a middle

Explain that every outlet provides its own value, so bare outlets reset it to undefined, and show the typed wrapper hook the docs recommend.

for a senior

Choose between outlet context, a React context provider and per-route data loading, and avoid coupling children to a parent's fetched data.

for a principal

Define the contract between layout and child routes as an explicit typed API, so teams can move routes without silent undefined values.

## The problem it solves A layout route often owns something its child routes need: the project object it already loaded, a selection, a callback to refresh a list. The child is not rendered by the layout directly; it is placed by `<Outlet />`, so the layout cannot pass props to it. React Router v7 provides a small built-in channel for exactly this case: the **`context` prop on `<Outlet>`** and the **`useOutletContext()`** hook. ## How it works ```tsx // Layout route: projects/:projectId function ProjectFrame() { const project = useCurrentProject(); // app code const [panel, setPanel] = useState<"open" | "closed">("open"); return <Outlet context={{ project, panel, setPanel }} />; } // Child route: tasks/:taskId function TaskScreen() { const { project, panel } = useOutletContext<ProjectOutlet>(); // ... } ``` Under the hood: 1. `useOutletContext` reads an internal React context created with a default of `null`. 2. `<Outlet context={x}>` renders the matched child route wrapped in a provider for that context with the value `x`. 3. Every `<Outlet>` renders its own provider. A bare `<Outlet />` provides `undefined`. ## How far the value reaches | Where `useOutletContext()` is called | Value received | |---|---| | the child route rendered in `ProjectFrame`'s outlet | `ProjectFrame`'s value | | any component inside that child route's element | `ProjectFrame`'s value | | a grandchild route rendered by the child's bare `<Outlet />` | `undefined` | | a grandchild route whose parent re-passes `context={useOutletContext()}` | `ProjectFrame`'s value | So the reach is **one routing level plus its component subtree**. That scoping is a feature: the value a child sees comes from the outlet that rendered it, and a deeper layout can provide something different to its own children. If several levels need the same object, either re-pass it at each outlet or put a normal `createContext` provider around the layout's output, which reaches every descendant regardless of outlets. ## Typing it `useOutletContext` is generic but not linked to the parent, so TypeScript cannot check the two ends agree. The React Router docs recommend that the parent route module export a **custom hook**: ```tsx type ProjectOutlet = { project: Project; panel: "open" | "closed"; setPanel(p: "open" | "closed"): void }; export function useProjectOutlet() { return useOutletContext<ProjectOutlet>(); } ``` Children import `useProjectOutlet()` instead of calling the raw hook. That gives one place that defines the type, makes consumers discoverable with a search, and lets the parent change the shape safely. Pairing it with `satisfies ProjectOutlet` on the `context` prop checks the provider side too. ## When to use it, and when not to Good fits: - UI state owned by the layout: a selected tab, a collapsed sidebar, a filter panel's open state; - a callback the layout provides, such as a function to refresh the project header after a child edits the name; - a value the layout already has in hand and every child needs. Poor fits: - **Data each child route should load for itself.** Passing a large object down just because the parent happened to fetch it couples the child to the parent's fetching and hides the child's real dependencies. - **Values needed far down the tree across several layouts.** An ordinary React context is clearer there. - **Global app state.** That belongs in a state store or a top-level provider, not in one layout's outlet. ## Common mistakes - **Expecting the value two levels down.** A grandchild behind a bare `<Outlet />` receives `undefined`, and destructuring it throws. - **Calling `useOutletContext()` in the layout itself.** The layout reads the context of the outlet that rendered **it**, which is its own parent's value, not the one it provides. - **Creating a new object on every render without need.** Each render passes a new `context` object, so every consumer re-renders with the layout; that is usually fine, but memoise the object if the layout re-renders often. - **Casting instead of typing.** `useOutletContext() as Project` in every child spreads an unchecked assumption across files; the wrapper hook keeps it in one place. ## Summary - `<Outlet context={value}>` provides; `useOutletContext()` reads. - Each outlet provides its own value; bare outlets provide `undefined`. - Export a typed wrapper hook from the parent. - Use it for layout-owned UI state and callbacks, not as a data-loading shortcut.

  • In React Router v7, a task route renders its own <Outlet /> for a comments child route. How does the comments route get the project?
    The task route must forward it: `<Outlet context={useOutletContext()} />`, or pass a new object that includes the project. A bare `<Outlet />` provides `undefined`, so without forwarding the comments route cannot see the project layout's value. If many levels need it, a React context provider in the project layout is simpler.
  • In React Router v7, why not pass a whole fetched project through outlet context to every child?
    It couples each child to how the parent loaded data and hides the child's real dependency. When a child is moved or rendered elsewhere, it silently receives nothing. Share layout-owned UI state and callbacks this way; let routes that need data load or select it themselves.

saying these in an interview costs you the question

  • useOutletContext reaches every descendant route below the layout
  • The outlet context type is inferred automatically from the parent
  • A bare <Outlet /> passes its parent's context through unchanged
  • Outlet context is the recommended way to share global app state
  • Child routes receive the context as props from the router