next
Functionnext()
future.unstable_middleware
flag to enable it.
Middleware allows you to run code before and after your route handlers (loaders, actions, and components) execute. This enables common patterns like authentication, logging, error handling, and data preprocessing in a reusable way.
Middleware runs in a nested chain, executing from parent routes to child routes on the way "down" to your route handlers, then from child routes back to parent routes on the way "up" after your handlers complete.
For example, on a GET /parent/child
request, the middleware would run in the following order:
- Root middleware start
- Parent middleware start
- Child middleware start
- Run loaders
- Child middleware end
- Parent middleware end
- Root middleware end
First, enable middleware in your React Router config:
import type { Config } from "@react-router/dev/config";
export default {
future: {
unstable_middleware: true,
},
} satisfies Config;
context
parameter to your loaders and actions. Please pay attention to the section on getLoadContext below if you are actively using context
today.
Update your app/types/global.d.ts
to enable middleware types:
declare module "@react-router/dev/routes" {
interface AppConfig {
future: {
unstable_middleware: true;
};
}
}
Create type-safe context objects using unstable_createContext
:
import { unstable_createContext } from "react-router";
import type { User } from "~/types";
export const userContext =
unstable_createContext<User | null>(null);
import { redirect } from "react-router";
import { userContext } from "~/context";
// Server-side Authentication Middleware
export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
[
async ({ request, context }) => {
const user = await getUserFromSession(request);
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
},
];
// Client-side timing middleware
export const unstable_clientMiddleware: Route.unstable_ClientMiddlewareFunction[] =
[
async ({ context }, next) => {
const start = performance.now();
await next();
const duration = performance.now() - start;
console.log(`Navigation took ${duration}ms`);
},
];
export async function loader({
context,
}: Route.LoaderArgs) {
const user = context.get(userContext);
const profile = await getProfile(user);
return { profile };
}
export default function Dashboard({
loaderData,
}: Route.ComponentProps) {
return (
<div>
<h1>Welcome {loaderData.profile.fullName}!</h1>
<Profile profile={loaderData.profile} />
</div>
);
}
Server middleware (unstable_middleware
) runs on the server for:
.data
requests for subsequent navigations and fetcher callsClient middleware (unstable_clientMiddleware
) runs in the browser for:
next
FunctionThe next
function runs the next middleware in the chain, or the route handlers if it's the leaf route middleware:
const middleware = async ({ context }, next) => {
// Code here runs BEFORE handlers
console.log("Before");
const response = await next();
// Code here runs AFTER handlers
console.log("After");
return response; // Optional on client, required on server
};
next()
once per middleware. Calling it multiple times will throw an error
next()
If you don't need to run code after your handlers, you can skip calling next()
:
const authMiddleware = async ({ request, context }) => {
const user = await getUser(request);
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
// next() is called automatically
};
The new context system provides type safety and prevents naming conflicts:
// โ
Type-safe
import { unstable_createContext } from "react-router";
const userContext = unstable_createContext<User>();
// Later in middleware/loaders
context.set(userContext, user); // Must be User type
const user = context.get(userContext); // Returns User type
// โ Old way (no type safety)
// context.user = user; // Could be anything
import { redirect } from "react-router";
import { userContext } from "~/context";
import { getSession } from "~/sessions.server";
export const authMiddleware = async ({
request,
context,
}) => {
const session = await getSession(request);
const userId = session.get("userId");
if (!userId) {
throw redirect("/login");
}
const user = await getUserById(userId);
context.set(userContext, user);
};
import { authMiddleware } from "~/middleware/auth";
export const unstable_middleware = [authMiddleware];
export function loader({ context }: Route.LoaderArgs) {
const user = context.get(userContext); // Guaranteed to exist
return { user };
}
import { requestIdContext } from "~/context";
export const loggingMiddleware = async (
{ request, context },
next
) => {
const requestId = crypto.randomUUID();
context.set(requestIdContext, requestId);
console.log(
`[${requestId}] ${request.method} ${request.url}`
);
const start = performance.now();
const response = await next();
const duration = performance.now() - start;
console.log(
`[${requestId}] Response ${response.status} (${duration}ms)`
);
return response;
};
export const errorMiddleware = async (
{ context },
next
) => {
try {
return await next();
} catch (error) {
// Log error
console.error("Route error:", error);
// Re-throw to let React Router handle it
throw error;
}
};
export const cmsFallbackMiddleware = async (
{ request },
next
) => {
const response = await next();
// Check if we got a 404
if (response.status === 404) {
// Check CMS for a redirect
const cmsRedirect = await checkCMSRedirects(
request.url
);
if (cmsRedirect) {
throw redirect(cmsRedirect, 302);
}
}
return response;
};
export const headersMiddleware = async (
{ context },
next
) => {
const response = await next();
// Add security headers
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
return response;
};
Client middleware works similarly but doesn't return responses:
import { userContext } from "~/context";
export const unstable_clientMiddleware = [
({ context }) => {
// Set up client-side user data
const user = getLocalUser();
context.set(userContext, user);
},
async ({ context }, next) => {
console.log("Starting client navigation");
await next();
console.log("Client navigation complete");
},
];
export async function clientLoader({
context,
}: Route.ClientLoaderArgs) {
const user = context.get(userContext);
return { user };
}
export const unstable_middleware = [
async ({ request, context }, next) => {
// Only run auth for POST requests
if (request.method === "POST") {
await ensureAuthenticated(request, context);
}
return next();
},
];
const sharedDataContext = unstable_createContext<any>();
export const unstable_middleware = [
async ({ request, context }, next) => {
if (request.method === "POST") {
// Set data during action phase
context.set(
sharedDataContext,
await getExpensiveData()
);
}
return next();
},
];
export async function action({
context,
}: Route.ActionArgs) {
const data = context.get(sharedDataContext);
// Use the data...
}
export async function loader({
context,
}: Route.LoaderArgs) {
const data = context.get(sharedDataContext);
// Same data is available here
}
If you're using a custom server, update your getLoadContext
function:
import { unstable_createContext } from "react-router";
import type { unstable_InitialContext } from "react-router";
const dbContext = unstable_createContext<Database>();
function getLoadContext(req, res): unstable_InitialContext {
const map = new Map();
map.set(dbContext, database);
return map;
}
If you're currently using AppLoadContext
, you can migrate most easily by creating a context for your existing object:
import { unstable_createContext } from "react-router";
declare module "@react-router/server-runtime" {
interface AppLoadContext {
db: Database;
user: User;
}
}
const myLoadContext =
unstable_createContext<AppLoadContext>();
Update your getLoadContext
function to return a Map with the context initial value:
function getLoadContext() {
const loadContext = {...};
- return loadContext;
+ return new Map([
+ [myLoadContext, appLoadContextInstance]]
+ );
}
Update your loaders/actions to read from the new context instance:
export function loader({ context }: Route.LoaderArgs) {
- const { db, user } = context;
+ const { db, user } = context.get(myLoadContext);
}