How to Pass Outlet Context to Nested Routes in React Router v6: Access Context in Child Components
React Router v6 revolutionized routing in React applications with a simplified API, improved route nesting, and better support for dynamic routes. One common challenge when working with nested routes is sharing data or functionality between a parent route and its child routes without resorting to prop drilling (passing props through multiple levels) or overusing global state management tools like Redux or Context API.
Enter Outlet Context—a built-in feature in React Router v6 that allows parent routes to pass data directly to their nested child routes. This pattern is lightweight, scoped to the route hierarchy, and ideal for sharing route-specific state or actions.
In this guide, we’ll dive deep into how Outlet Context works, how to pass context from parent to child routes, and how to access that context in child components. We’ll also walk through a practical example, highlight common pitfalls, and share best practices to ensure you use this feature effectively.
Table of Contents#
- Understanding React Router v6’s Outlet Component
- What is Outlet Context?
- Step 1: Passing Context with the Outlet Component
- Step 2: Accessing Context in Child Routes with useOutletContext
- Practical Example: Dashboard with Nested Routes
- TypeScript Support for Outlet Context
- Common Pitfalls to Avoid
- Best Practices
- Outlet Context vs. React Context API: When to Use Which?
- Conclusion
- References
Understanding React Router v6’s Outlet Component#
Before we explore Outlet Context, let’s recap the role of the Outlet component in React Router v6.
In React Router, nested routes are defined using the Route component’s element prop and nested Route children. The Outlet component acts as a "placeholder" in the parent route’s component where the child route’s element will be rendered.
For example, if you have a parent route /dashboard with nested routes /dashboard/profile and /dashboard/settings, the parent Dashboard component will render an Outlet to display the Profile or Settings component when their respective paths are active.
Basic Outlet Usage:
// Dashboard.js (Parent Route Component)
import { Outlet, Link } from "react-router-dom";
export default function Dashboard() {
return (
<div>
<nav>
<Link to="profile">Profile</Link>
<Link to="settings">Settings</Link>
</nav>
{/* Child routes render here */}
<Outlet />
</div>
);
} In this example, <Outlet /> is where the child route components (Profile or Settings) will be injected when the user navigates to /dashboard/profile or /dashboard/settings.
What is Outlet Context?#
Outlet Context is a mechanism in React Router v6 that lets you pass data from a parent route component to its direct nested child route components via the Outlet component. This data can be any valid JavaScript value: objects, arrays, functions, primitives, or even React state.
Think of it as "route-scoped context"—it’s only available to the child routes directly nested under the parent, making it perfect for sharing state or actions that are specific to a route hierarchy (e.g., a dashboard’s user data or navigation helpers).
Key Features:
- Scoped to parent-child route relationships (no global pollution).
- Lightweight alternative to prop drilling or global context for route-specific data.
- Supports any data type, including functions (e.g., callbacks for parent-child communication).
Step 1: Passing Context with the Outlet Component#
To pass context from a parent route to its children, use the context prop on the Outlet component. The context prop accepts any value, but it’s most common to pass an object containing multiple values (e.g., state and functions).
Syntax:
<Outlet context={/* Your context value here */} /> Example: Parent Route Passing Context
Suppose the Dashboard parent component manages a user object and a handleLogout function. We want to pass both to its child routes:
// Dashboard.js (Parent Route Component)
import { Outlet, Link, useNavigate } from "react-router-dom";
import { useState } from "react";
export default function Dashboard() {
const [user, setUser] = useState({ name: "Alice", email: "[email protected]" });
const navigate = useNavigate();
const handleLogout = () => {
// Logout logic (e.g., clear auth state)
setUser(null);
navigate("/login");
};
return (
<div>
<h1>Welcome, {user.name}!</h1>
<nav>
<Link to="profile">Profile</Link>
<Link to="settings">Settings</Link>
<button onClick={handleLogout}>Logout</button>
</nav>
{/* Pass user and handleLogout to child routes via Outlet context */}
<Outlet context={{ user, handleLogout }} />
</div>
);
} Here, <Outlet context={{ user, handleLogout }} /> passes an object with user and handleLogout to all direct child routes of /dashboard.
Step 2: Accessing Context in Child Routes with useOutletContext#
To access the context passed via Outlet in a child route component, use React Router’s useOutletContext hook. This hook returns the context value provided by the nearest parent Outlet.
Syntax:
import { useOutletContext } from "react-router-dom";
export default function ChildComponent() {
const context = useOutletContext();
// Use context values
} Example: Child Route Accessing Context
Let’s access the user and handleLogout context in the Profile child component:
// Profile.js (Child Route Component)
import { useOutletContext } from "react-router-dom";
export default function Profile() {
// Access context from parent Dashboard
const { user, handleLogout } = useOutletContext();
return (
<div>
<h2>Profile</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<button onClick={handleLogout}>Logout from Profile</button>
</div>
);
} In this example, Profile retrieves user and handleLogout from the parent Dashboard via useOutletContext(), allowing it to display user data and trigger logout.
Practical Example: Dashboard with Nested Routes#
Let’s build a complete example to solidify this concept. We’ll create:
- A root
Appcomponent with routing setup. - A
Dashboardparent route component that passes context. - Two child routes:
ProfileandSettings, which access the context.
Step 1: Set Up Routing in App.js#
First, define the route hierarchy in App.js. The /dashboard route will be the parent, with nested routes for profile and settings.
// App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Dashboard from "./Dashboard";
import Profile from "./Profile";
import Settings from "./Settings";
import Login from "./Login";
export default function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
{/* Parent route with nested children */}
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<p>Welcome to your dashboard!</p>} /> {/* Default child */}
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</Router>
);
} Step 2: Parent Dashboard Component with Context#
The Dashboard component will manage user state and a handleLogout function, passing both to child routes via Outlet context.
// Dashboard.js
import { Outlet, Link, useNavigate } from "react-router-dom";
import { useState } from "react";
export default function Dashboard() {
const [user, setUser] = useState({
name: "Alice Smith",
email: "[email protected]",
membership: "Premium"
});
const navigate = useNavigate();
const handleLogout = () => {
// Simulate logout: clear user and redirect to login
setUser(null);
navigate("/login");
};
return (
<div className="dashboard">
<header>
<h1>Dashboard</h1>
<p>Logged in as: {user.name}</p>
</header>
<nav>
<Link to="/dashboard">Home</Link>
<Link to="profile">Profile</Link>
<Link to="settings">Settings</Link>
<button onClick={handleLogout}>Logout</button>
</nav>
<main>
{/* Pass context to child routes */}
<Outlet context={{ user, handleLogout, setUser }} />
</main>
</div>
);
} Step 3: Child Route: Profile Component#
The Profile component accesses user from the context to display user details.
// Profile.js
import { useOutletContext } from "react-router-dom";
export default function Profile() {
// Access context from parent Dashboard
const { user } = useOutletContext();
return (
<div className="profile">
<h2>Your Profile</h2>
<div>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<p>Membership: {user.membership}</p>
</div>
</div>
);
} Step 4: Child Route: Settings Component#
The Settings component uses both user and setUser (from context) to let the user update their name.
// Settings.js
import { useOutletContext, useState } from "react-router-dom";
export default function Settings() {
// Access user and setUser from parent context
const { user, setUser } = useOutletContext();
const [newName, setNewName] = useState(user.name);
const handleUpdateName = (e) => {
e.preventDefault();
setUser({ ...user, name: newName }); // Update user in parent state
};
return (
<div className="settings">
<h2>Settings</h2>
<form onSubmit={handleUpdateName}>
<label>
Update Name:
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
/>
</label>
<button type="submit">Save</button>
</form>
</div>
);
} How It Works#
- When the user navigates to
/dashboard/profile, theProfilecomponent renders insideDashboard’sOutletand accessesuserviauseOutletContext. - When the user updates their name in
Settings,setUser(passed via context) updates the parentDashboard’s state, causing all child components to re-render with the newuserdata.
TypeScript Support for Outlet Context#
If you’re using TypeScript, you can type the Outlet Context for better type safety. Define an interface for the context and pass it as a generic to useOutletContext.
Example with TypeScript:
// Dashboard.tsx (Parent with Typed Context)
import { Outlet, useNavigate } from "react-router-dom";
import { useState } from "react";
// Define context type
interface DashboardContext {
user: { name: string; email: string };
handleLogout: () => void;
setUser: (user: { name: string; email: string }) => void;
}
export default function Dashboard() {
const [user, setUser] = useState<DashboardContext["user"]>({
name: "Alice",
email: "[email protected]"
});
const navigate = useNavigate();
const handleLogout: DashboardContext["handleLogout"] = () => {
setUser({ name: "", email: "" });
navigate("/login");
};
return (
<div>
<h1>Welcome, {user.name}!</h1>
{/* Pass typed context */}
<Outlet context={{ user, handleLogout, setUser } as DashboardContext} />
</div>
);
} Child Component with Typed Context:
// Profile.tsx
import { useOutletContext } from "react-router-dom";
import { DashboardContext } from "./Dashboard"; // Import the interface
export default function Profile() {
// Use generic to enforce context type
const { user } = useOutletContext<DashboardContext>();
return <p>Name: {user.name}</p>; // TypeScript knows user has a `name` property
} Common Pitfalls to Avoid#
1. Context Only Flows to Direct Nested Routes#
Outlet Context is scoped to the immediate parent-child route relationship. If you have a deeply nested route (e.g., /dashboard/settings/notifications), the notifications component will not receive context from /dashboard unless the intermediate settings component explicitly passes it down via its own Outlet.
Fix: Pass context through each level of Outlet in nested parents.
2. Using useOutletContext in Non-Child Components#
The useOutletContext hook only works in components rendered by a child route of the Outlet that provided the context. Using it in unrelated components (e.g., a sibling route or a component outside the route hierarchy) will return undefined.
Fix: Ensure the component is a direct child of the route with the Outlet.
3. Mutating Context Directly#
If you pass an object or array via context and mutate it (e.g., user.name = "Bob"), React may not re-render child components, leading to stale data.
Fix: Always update context immutably (e.g., setUser({ ...user, name: "Bob" })).
4. Forgetting to Render Outlet#
If the parent route component doesn’t render an Outlet, child routes will not render, and context cannot be passed.
Fix: Always include <Outlet /> in parent route components with nested children.
Best Practices#
1. Keep Context Focused#
Only pass data/functions that child routes actually need. Avoid dumping large objects into context (it bloats re-renders).
2. Use Custom Hooks for Reusability#
In TypeScript, wrap useOutletContext in a custom hook to avoid repeating the context type everywhere:
// useDashboardContext.ts
import { useOutletContext } from "react-router-dom";
import { DashboardContext } from "./Dashboard";
export function useDashboardContext() {
return useOutletContext<DashboardContext>();
} Usage in Child Components:
import { useDashboardContext } from "./useDashboardContext";
export default function Profile() {
const { user } = useDashboardContext();
// ...
} 3. Document Context Shape#
Add comments or TypeScript interfaces to clarify what context contains. This helps teammates understand what’s available.
4. Prefer Outlet Context Over Global Context for Route-Specific State#
If the data is only relevant to a route hierarchy, Outlet Context is cleaner than a global React Context. Reserve global Context for app-wide state (e.g., theme, auth).
Outlet Context vs. React Context API: When to Use Which?#
| Outlet Context | React Context API |
|---|---|
| Scoped to parent-child route relationships. | Global (or scoped to a component subtree). |
Lightweight, no setup beyond Outlet prop. | Requires creating a Context, Provider, etc. |
| Ideal for route-specific state/actions. | Ideal for app-wide or cross-cutting state. |
Uses useOutletContext hook. | Uses useContext hook. |
Example Decision Tree:
- Use Outlet Context if: Sharing data between a dashboard and its nested profile/settings routes.
- Use React Context if: Sharing theme preferences across the entire app.
Conclusion#
Outlet Context in React Router v6 is a powerful tool for sharing state and actions between parent and nested child routes. By using the Outlet component’s context prop and the useOutletContext hook, you can avoid prop drilling and keep your codebase clean and focused.
Key takeaways:
Outletrenders child routes and passes context via thecontextprop.useOutletContextretrieves context in child route components.- Context is scoped to direct parent-child routes.
- Prefer Outlet Context for route-specific data; use React Context for global state.