Middleware
On this page

Middleware



The middleware feature is currently experimental and subject to breaking changes. Use the future.unstable_middleware flag to enable it.

Middleware allows you to run code before and after the Response generation for the matched path. 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 a Response is generated.

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, generate HTML Response
    - Child middleware end
  - Parent middleware end
- Root middleware end

There are some slight differences between middleware on the server (framework mode) versus the client (framework/data mode). For the purposes of this document, we'll be referring to Server Middleware in most of our examples as it's the most familiar to users who've used middleware in other HTTP servers in the past. Please refer to the Server vs Client Middleware section below for more information.

Quick Start (Framework mode)

1. Enable the middleware flag

First, enable middleware in your React Router config:

import type { Config } from "@react-router/dev/config";

export default {
  future: {
    unstable_middleware: true,
  },
} satisfies Config;

By enabling the middleware feature, you change the type of the context parameter to your loaders and actions. Please pay attention to the section on getLoadContext below if you are actively using context today.

2. Create a context

Middleware uses a context provider instance to provide data down the middleware chain. You can 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);

3. Export middleware from your routes

import { redirect } from "react-router";
import { userContext } from "~/context";

// Server-side Authentication Middleware
async function authMiddleware({ request, context }) {
  const user = await getUserFromSession(request);
  if (!user) {
    throw redirect("/login");
  }
  context.set(userContext, user);
}

export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
  [authMiddleware];

// Client-side timing middleware
async function timingMiddleware({ context }, next) {
  const start = performance.now();
  await next();
  const duration = performance.now() - start;
  console.log(`Navigation took ${duration}ms`);
}

export const unstable_clientMiddleware: Route.unstable_ClientMiddlewareFunction[] =
  [timingMiddleware];

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>
  );
}

4. Update your getLoadContext function (if applicable)

If you're using a custom server and a getLoadContext function, you will need to update your implementation to return an instance of unstable_RouterContextProvider, instead of a JavaScript object:

+import {
+  unstable_createContext,
+  unstable_RouterContextProvider,
+} from "react-router";
import { createDb } from "./db";

+const dbContext = unstable_createContext<Database>();

function getLoadContext(req, res) {
-  return { db: createDb() };
+  const context = new unstable_RouterContextProvider();
+  context.set(dbContext, createDb());
+  return context;
}

Quick Start (Data Mode)

1. Enable the middleware flag

const router = createBrowserRouter(routes, {
  future: {
    unstable_middleware: true,
  },
});

2. Create a context

Middleware uses a context provider instance to provide data down the middleware chain. You can 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);

3. Add middleware to your routes

import { redirect } from "react-router";
import { userContext } from "~/context";

const routes = [
  {
    path: "/",
    unstable_middleware: [timingMiddleware], // 👈
    Component: Root,
    children: [
      {
        path: "profile",
        unstable_middleware: [authMiddleware], // 👈
        loader: profileLoader,
        Component: Profile,
      },
      {
        path: "login",
        Component: Login,
      },
    ],
  },
];

async function timingMiddleware({ context }, next) {
  const start = performance.now();
  await next();
  const duration = performance.now() - start;
  console.log(`Navigation took ${duration}ms`);
}

async function authMiddleware({ context }) {
  const user = await getUser();
  if (!user) {
    throw redirect("/login");
  }
  context.set(userContext, user);
}

export async function profileLoader({
  context,
}: Route.LoaderArgs) {
  const user = context.get(userContext);
  const profile = await getProfile(user);
  return { profile };
}

export default function Profile() {
  let loaderData = useLoaderData();
  return (
    <div>
      <h1>Welcome {loaderData.profile.fullName}!</h1>
      <Profile profile={loaderData.profile} />
    </div>
  );
}

4. Add an unstable_getContext() function (optional)

If you wish to include a base context on all navigations/fetches, you can add an unstable_getContext function to your router. This will be called to populate a fresh context on every navigation/fetch.

let sessionContext = unstable_createContext();

const router = createBrowserRouter(routes, {
  future: {
    unstable_middleware: true,
  },
  unstable_getContext() {
    let context = new unstable_RouterContextProvider();
    context.set(sessionContext, getSession());
    return context;
  },
});

This API exists to mirror the getLoadContext API on the server in Framework Mode, which exists as a way to hand off values from your HTTP server to the React Router handler. This unstable_getContext API can be used to hand off global values from the window/document to React Router, but because they're all running in the same context (the browser), you can achieve effectively the the same behavior with a root route middleware. Therefore, you may not need this API the same way you would on the server - but it's provided for consistency.

Core Concepts

Server vs Client Middleware

Server middleware runs on the server in Framework mode for HTML Document requests and .data requests for subsequent navigations and fetcher calls. Because server middleware runs on the server in response to an HTTP Request, it returns an HTTP Response back up the middleware chain via the next function:

async function serverMiddleware({ request }, next) {
  console.log(request.method, request.url);
  let response = await next();
  console.log(response.status, request.method, request.url);
  return response;
}

// Framework mode only
export const unstable_middleware = [serverMiddleware];

Client middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware is different because there's no HTTP Request, so it doesn't bubble up anything via the next function:

async function clientMiddleware({ request }, next) {
  console.log(request.method, request.url);
  await next(); // 👈 No return value
  console.log(response.status, request.method, request.url);
  // 👈 No need to return anything here
}

// Framework mode
export const unstable_clientMiddleware = [clientMiddleware];

// Or, Data mode
const route = {
  path: "/",
  unstable_middleware: [clientMiddleware],
  loader: rootLoader,
  Component: Root,
};

When Middleware Runs

It is very important to understand when your middlewares will run to make sure your application is behaving as you intend.

Server Middleware

In a hydrated Framework Mode app, server middleware is designed such that it prioritizes SPA behavior and does not create new network activity by default. Middleware wraps existing requests and only runs when you need to hit the server.

This raises the question of what is a "handler" in React Router? Is it the route? Or the loader? We think "it depends":

  • On document requests (GET /route), the handler is the route - because the response encompasses both the loader and the route component
  • On data requests (GET /route.data) for client-side navigations, the handler is the loader/action, because that's all that is included in the response

Therefore:

  • Document requests run server middleware whether loaders exist or not because we're still in a "handler" to render the UI
  • Client-side navigations will only run server middleware if a .data request is made to the server for a loader/action

This is important behavior for request-annotation middlewares such as logging request durations, checking/setting sessions, setting outgoing caching headers, etc. It would be useless to go to the server and run those types of middlewares when there was no reason to go to the server in the first place. This would result in increased server load and noisy server logs.

// This middleware won't run on client-side navigations without a `.data` request
function loggingMiddleware({ request }, next) {
  console.log(`Request: ${request.method} ${request.url}`);
  let response = await next();
  console.log(
    `Response: ${response.status} ${request.method} ${request.url}`,
  );
  return response;
}

export const unstable_middleware = [loggingMiddleware];

However, there may be cases where you want to run certain server middlewares on every client-navigation - even if no loader exists. For example, a form in the authenticated section of your site that doesn't require a loader but you'd rather use auth middleware to redirect users away before they fill out the form - rather then when they submit to the action. If your middleware meets this criteria, then you can put a loader on the route that contains the middleware to force it to always call the server for client side navigations involving that route.

function authMiddleware({ request }, next) {
  if (!isLoggedIn(request)) {
    throw redirect("/login");
  }
}

export const unstable_middleware = [authMiddleware];

// By adding a loader, we force the authMiddleware to run on every client-side
// navigation involving this route.
export function loader() {
  return null;
}

Client Middleware

Client middleware is simpler because since we are already on the client and are always making a "request" to the router when navigating. Client middlewares will run on every client navigation, regardless of whether or not there are loaders to run.

Context API

The new context system provides type safety and prevents naming conflicts and allows you to provide data to nested middlewares and loader/action functions. In Framework Mode, this replaces the previous AppLoadContext API.

// ✅ 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

Context and AsyncLocalStorage

Node provides an AsyncLocalStorage API which gives you a way to provide values through asynchronous execution contexts. While this is a Node API, most modern runtimes have have made it (mostly) available (i.e., Cloudflare, Bun, Deno).

In theory, we could have leveraged AsyncLocalStorage directly as the way to pass values from middlewares to to child routes, but the lack of 100% cross-platform compatibility was concerning enough that we wanted to still ship a first-class context API so there would be way to publish reusable middleware packages guaranteed to work in a runtime-agnostic manner.

That said, this API still works great with React Router middleware and can be used in place of, or alongside of the context API:

AsyncLocalStorage is especially powerful when using React Server Components because it allows you to provide information from middleware to your Server Components and Server Actions because they run in the same server execution context 🤯

import { AsyncLocalStorage } from "node:async_hooks";

const USER = new AsyncLocalStorage<User>();

export async function provideUser(
  request: Request,
  cb: () => Promise<Response>,
) {
  let user = await getUser(request);
  return USER.run(user, cb);
}

export function getUser() {
  return USER.getStore();
}
import { provideUser } from "./user-context";

export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
  [
    async ({ request, context }, next) => {
      return provideUser(request, async () => {
        let res = await next();
        return res;
      });
    },
  ];
import { getUser } from "../user-context";

export function loader() {
  let user = getUser();
  //...
}

The next Function

The next function logic depends on which route middleware it's being called from:

  • When called from a non-leaf middleware, it runs the next middleware in the chain
  • When called from the leaf middleware, it executes any route handlers and generates the resulting Response for the request
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
};

You can only call next() once per middleware. Calling it multiple times will throw an error

Skipping 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
};

next() and Error Handling

React Router contains built-in error handling via the route ErrorBoundary export. Just like when a loader/action throws (mostly), if a middleware throws it will be caught and handled at the appropriate ErrorBoundary and a Response will be returned through the ancestor next() call. This means that the next() function should never throw and should always return a Response, so you don't need to worry about wrapping it in a try/catch.

This behavior is important to allow middleware patterns such as automatically setting required headers on outgoing responses (i.e., committing a session) from a root middleware. If any error from a middleware caused next() to throw, we'd miss the execution of ancestor middlewares on the way out and those required headers wouldn't be set.

// routes/parent.tsx
export const unstable_middleware = [
  async (_, next) => {
    let res = await next();
    //  ^ res.status = 500
    // This response contains the ErrorBoundary
    return res;
  }
]

// routes/parent.child.tsx
export const unstable_middleware = [
  async (_, next) => {
    let res = await next();
    //  ^ res.status = 200
    // This response contains the successful UI render
    throw new Error('Uh oh, something went wrong!)
  }
]

We say "mostly" above because there is a small/nuanced difference if you throw a non-redirect Response in a loader/action versus throwing a Response in a middleware (redirects behave the same).

Throwing a non-redirect response from a middleware will use that response directly and return it up through the parent next() call. This differs from the behavior in loaders/actions where that response will be converted to an ErrorResponse to be rendered by the ErrorBoundary.

The difference here is because loaders/actions are expected to return data which is then provided to components for rendering. But middleware is expected to return a response - so if you return or throw one, we will use it directly. If you want to throw an error with a status code to an error boundary from middleware, you should use the data utility.

Changes to getLoadContext/AppLoadContext

This only applies if you are using a custom server and a custom getLoadContext function

Middleware introduces a breaking change to the context parameter generated by getLoadContext and passed to your loaders and actions. The current approach of a module-augmented AppLoadContext isn't really type-safe and instead just sort of tells TypeScript to "trust me".

Middleware needs an equivalent context on the client for clientMiddleware, but we didn't want to duplicate this pattern from the server that we already weren't thrilled with, so we decided to introduce a new API where we could tackle type-safety.

When opting into middleware, the context parameter changes to an instance of RouterContextProvider:

let dbContext = unstable_createContext<Database>();
let context = new unstable_RouterContextProvider();
context.set(dbContext, getDb());
//                     ^ type-safe
let db = context.get(dbContext);
//  ^ Database

If you're using a custom server and a getLoadContext function, you will need to update your implementation to return an instance of unstable_RouterContextProvider, instead of a plain JavaScript object:

+import {
+  unstable_createContext,
+  unstable_RouterContextProvider,
+} from "react-router";
import { createDb } from "./db";

+const dbContext = unstable_createContext<Database>();

function getLoadContext(req, res) {
-  return { db: createDb() };
+  const context = new unstable_RouterContextProvider();
+  context.set(dbContext, createDb());
+  return context;
}

Migration from AppLoadContext

If you're currently using AppLoadContext, you can migrate incrementally by using your existing module augmentation to augment unstable_RouterContextProvider instead of AppLoadContext. Then, update your getLoadContext function to return an instance of unstable_RouterContextProvider:

declare module "react-router" {
-  interface AppLoadContext {
+  interface unstable_RouterContextProvider {
    db: Database;
    user: User;
  }
}

function getLoadContext() {
  const loadContext = {...};
-  return loadContext;
+  let context = new unstable_RouterContextProvider();
+  Object.assign(context, loadContext);
+  return context;
}

This allows you to leave your loaders/actions untouched during initial adoption of middleware, since they can still read values directly (i.e., context.db).

This approach is only intended to be used as a migration strategy when adopting middleware in React Router v7, allowing you to incrementally migrate to context.set/context.get. It is not safe to assume this approach will work in the next major version of React Router.

The unstable_RouterContextProvider class is also used for the client-side context parameter via <HydratedRouter unstable_getContext> and <RouterProvider unstable_getContext>. Since AppLoadContext is primarily intended as a hand-off from your HTTP server into the React Router handlers, you need to be aware that these augmented fields will not be available in clientMiddleware, clientLoader, or clientAction functions even thought TypeScript will tell you they are (unless, of course, you provide the fields via unstable_getContext on the client).

Common Patterns

Authentication

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 };
}

Logging

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;
};

404 to CMS Redirect

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;
};

Response Headers

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;
};

Conditional Middleware

export const unstable_middleware = [
  async ({ request, context }, next) => {
    // Only run auth for POST requests
    if (request.method === "POST") {
      await ensureAuthenticated(request, context);
    }
    return next();
  },
];

Sharing Context Between Action and Loader

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
}
Docs and examples CC 4.0
Edit