A Beginner's Guide to Middleware in React Router

If you’ve ever needed to check authentication before every protected route, log requests across your application, or share data between parent and child routes in React Router, you’ve likely written the same code multiple times. React Router middleware solves this repetition problem by letting you intercept and process requests before they reach your loaders and actions.
This guide covers everything you need to implement React Router middleware in version 7.9+, from enabling the v8_middleware
flag to creating your first authentication middleware with the new middleware API.
Key Takeaways
- React Router 7 introduces middleware for handling cross-cutting concerns like authentication and logging
- Middleware runs sequentially in a nested chain, enabling efficient data sharing between routes
- The new context API provides type-safe data sharing without naming conflicts
- Both server and client middleware are supported with different execution behaviors
What is React Router Middleware?
React Router 7 middleware is a feature that runs code before and after your route handlers execute. Think of it as a processing pipeline where each middleware can inspect requests, add data to the context, or even short-circuit the request with a redirect.
Unlike the parallel execution of loaders in React Router 6, middleware runs sequentially in a nested chain. Parent middleware executes before child middleware on the way down, then in reverse order on the way up after generating a response.
Parent middleware → Child middleware → Route handler → Child middleware → Parent middleware
This sequential execution enables patterns that were previously impossible, like sharing authenticated user data from a parent route to all child routes without redundant database calls.
Enabling Middleware in Your Project
To start using middleware, first ensure you’re running React Router 7.9.0 or later (or 7.3.0+ with the middleware flag enabled). Then enable the feature flag in your configuration:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
future: {
v8_middleware: true,
},
} satisfies Config;
This flag enables stable middleware support and the new context API. The context
parameter in your loaders and actions now provides access to middleware-set data through a type-safe API.
Understanding the Context API
The new context API in React Router 7 provides a type-safe Map-like interface for sharing data between middleware and route handlers. Instead of attaching properties directly to a context object, you now use context.set()
and context.get()
with typed context keys:
import { createContext } from "react-router";
// Create a typed context key
const userContext = createContext<User>();
// In middleware
context.set(userContext, user);
// In loader/action
const user = context.get(userContext); // Type-safe User object
This approach eliminates TypeScript type assertions and prevents naming conflicts between different middleware.
Creating Your First Middleware
Let’s build an authentication middleware that protects routes and shares user data:
// app/middleware/auth.ts
import { redirect, createContext } from "react-router";
import type { Route } from "./+types/root";
export const userContext = createContext<User | null>();
export const authMiddleware: Route.Middleware = async ({
request,
context,
next
}) => {
const user = await getUserFromSession(request);
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
return next();
};
Apply this middleware to protected routes:
// app/routes/dashboard.tsx
import { authMiddleware, userContext } from "~/middleware/auth";
export const middleware: Route.MiddlewareFunction[] = [authMiddleware];
export async function loader({ context }: Route.LoaderArgs) {
const user = context.get(userContext); // Guaranteed to exist
const profile = await getProfile(user.id);
return { profile };
}
The middleware runs before the loader, ensuring authenticated access and providing the user object without additional database queries.
Discover how at OpenReplay.com.
Server Middleware vs Client Middleware
React Router supports both server and client middleware with slightly different behaviors:
Server middleware runs on the server and must return a Response:
export const middleware: Route.MiddlewareFunction[] = [
async ({ request }, next) => {
console.log(`Server: ${request.method} ${request.url}`);
const response = await next();
response.headers.set("X-Custom-Header", "value");
return response; // Required
};
];
Client middleware runs in the browser during client-side navigation:
export const middleware: Route.ClientMiddlewareFunction[] = [
async ({ next }) => {
const start = performance.now();
await next();
console.log(`Navigation took ${performance.now() - start}ms`);
},
];
Common Middleware Patterns
Logging Middleware
export const loggingMiddleware: Route.Middleware = async ({ request, next }) => {
const requestId = crypto.randomUUID();
console.log(`[${requestId}] ${request.method} ${request.url}`);
const response = await next();
console.log(`[${requestId}] Response ${response.status}`);
return response;
};
Composing Multiple Middleware
You can chain multiple middleware functions together:
// app/routes/admin.tsx
export const middleware = [
loggingMiddleware,
authMiddleware,
adminRoleMiddleware
];
Each middleware executes in order, and any can short-circuit the chain by throwing a redirect or error response.
Conclusion
React Router middleware transforms how you handle cross-cutting concerns in your application. By enabling the v8_middleware
flag and adopting the new context API, you can eliminate code duplication, share data efficiently between routes, and implement authentication, logging, and other patterns in a clean, reusable way.
Start with simple middleware like authentication or logging, then explore advanced patterns as your application grows. The sequential execution model and type-safe context API make middleware a powerful addition to your React Router toolkit.
FAQs
No, middleware is a new feature introduced behind a flag in React Router 7.3, stabilized in 7.9 via `future.v8_middleware`. Earlier versions require implementing similar functionality through wrapper components or custom loader logic.
Middleware can improve performance by reducing redundant operations. Since middleware runs sequentially and can share data through context, you avoid duplicate database queries or API calls that might occur with parallel loaders.
When middleware throws an error, React Router handles it like any other route error. The error bubbles up to the nearest error boundary, and subsequent middleware in the chain won't execute.
Middleware cannot directly modify the request object, but it can add data to the context that loaders and actions can access. For request modifications, create a new Request object and pass it to the next middleware.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.