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?
answer
- a prop on the outlet
- a hook in the child
- one outlet, one level
- a typed wrapper hook
basics
~20 sThe 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 linesimport { 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
Recall that a layout passes values with <Outlet context={...} /> and a child route reads them with useOutletContext().
Explain that every outlet provides its own value, so bare outlets reset it to undefined, and show the typed wrapper hook the docs recommend.
Choose between outlet context, a React context provider and per-route data loading, and avoid coupling children to a parent's fetched data.
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