UNPKG

23.6 kB Markdown View Raw
1---
2title: Middleware
3---
4
5# Middleware
6
7[MODES: framework, data]
8
9Middleware allows you to run code before and after the [`Response`][Response] generation for the matched path. This enables [common patterns][common-patterns] like authentication, logging, error handling, and data preprocessing in a reusable way.
10
11Middleware 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`][Response] is generated.
12
13For example, on a `GET /parent/child` request, the middleware would run in the following order:
14
15```text
16- Root middleware start
17 - Parent middleware start
18 - Child middleware start
19 - Run loaders, generate HTML Response
20 - Child middleware end
21 - Parent middleware end
22- Root middleware end
23```
24
25<docs-info>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][server-client] section below for more information.</docs-info>
26
27## Quick Start (Framework mode)
28
29### 1. Create a context
30
31Middleware uses a `context` provider instance to provide data down the middleware chain.
32You can create type-safe context objects using [`createContext`][createContext]:
33
34```ts filename=app/context.ts
35import { createContext } from "react-router";
36import type { User } from "~/types";
37
38export const userContext = createContext<User | null>(null);
39```
40
41### 2. Export middleware from your routes
42
43```tsx filename=app/routes/dashboard.tsx
44import { redirect } from "react-router";
45import { userContext } from "~/context";
46
47// Server-side Authentication Middleware
48async function authMiddleware({ request, context }) {
49 const user = await getUserFromSession(request);
50 if (!user) {
51 throw redirect("/login");
52 }
53 context.set(userContext, user);
54}
55
56export const middleware: Route.MiddlewareFunction[] = [
57 authMiddleware,
58];
59
60// Client-side timing middleware
61async function timingMiddleware({ context }, next) {
62 const start = performance.now();
63 await next();
64 const duration = performance.now() - start;
65 console.log(`Navigation took ${duration}ms`);
66}
67
68export const clientMiddleware: Route.ClientMiddlewareFunction[] =
69 [timingMiddleware];
70
71export async function loader({
72 context,
73}: Route.LoaderArgs) {
74 const user = context.get(userContext);
75 const profile = await getProfile(user);
76 return { profile };
77}
78
79export default function Dashboard({
80 loaderData,
81}: Route.ComponentProps) {
82 return (
83 <div>
84 <h1>Welcome {loaderData.profile.fullName}!</h1>
85 <Profile profile={loaderData.profile} />
86 </div>
87 );
88}
89```
90
91### 3. Add a `getLoadContext` function (if applicable)
92
93If you're using a custom server, you can use a `getLoadContext` function to pass information to into the react router handlers:
94
95```tsx
96import { RouterContextProvider } from "react-router";
97import { dbContext, createDb } from "./db";
98
99function getLoadContext(req, res) {
100 const context = new RouterContextProvider();
101 context.set(dbContext, createDb());
102 return context;
103}
104```
105
106## Quick Start (Data Mode)
107
108### 1. Create a context
109
110Middleware uses a `context` provider to pass data through the middleware chain into loaders and actions. Create typed context with [`createContext`][createContext]:
111
112```ts
113import { createContext } from "react-router";
114import type { User } from "~/types";
115
116export const userContext = createContext<User | null>(null);
117```
118
119### 2. Add `middleware` to route objects
120
121Attach `middleware` arrays to your route objects:
122
123```tsx
124import {
125 redirect,
126 useLoaderData,
127 type LoaderFunctionArgs,
128} from "react-router";
129import { userContext } from "~/context";
130
131const routes = [
132 {
133 path: "/",
134 middleware: [timingMiddleware], // 👈
135 Component: Root,
136 children: [
137 {
138 path: "dashboard",
139 middleware: [authMiddleware], // 👈
140 loader: dashboardLoader,
141 Component: Dashboard,
142 },
143 {
144 path: "login",
145 Component: Login,
146 },
147 ],
148 },
149];
150
151async function timingMiddleware({ context }, next) {
152 const start = performance.now();
153 await next();
154 const duration = performance.now() - start;
155 console.log(`Navigation took ${duration}ms`);
156}
157
158async function authMiddleware({ context }) {
159 const user = await getUser();
160 if (!user) {
161 throw redirect("/login");
162 }
163 context.set(userContext, user);
164}
165
166export async function dashboardLoader({
167 context,
168}: LoaderFunctionArgs) {
169 const user = context.get(userContext);
170 const profile = await getProfile(user);
171 return { profile };
172}
173
174export default function Dashboard() {
175 let loaderData = useLoaderData();
176 return (
177 <div>
178 <h1>Welcome {loaderData.profile.fullName}!</h1>
179 <Profile profile={loaderData.profile} />
180 </div>
181 );
182}
183```
184
185### 3. Add a `getContext` function (optional)
186
187To seed every navigation or fetcher call with shared values, pass [`getContext`][getContext] when creating the router:
188
189```tsx
190let sessionContext = createContext();
191
192const router = createBrowserRouter(routes, {
193 getContext() {
194 let context = new RouterContextProvider();
195 context.set(sessionContext, getSession());
196 return context;
197 },
198});
199```
200
201<docs-info>This mirrors Framework mode’s server-side [`getLoadContext`][getloadcontext]. In the browser, root `middleware` can often do the same job, but `getContext` is available when you want to seed every request up front.</docs-info>
202
203## Core Concepts
204
205### Server vs Client Middleware
206
207Server 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`][request], it returns an HTTP [`Response`][Response] back up the middleware chain via the `next` function:
208
209```ts
210async function serverMiddleware({ request }, next) {
211 console.log(request.method, request.url);
212 let response = await next();
213 console.log(response.status, request.method, request.url);
214 return response;
215}
216
217// Framework mode only
218export const middleware: Route.MiddlewareFunction[] = [
219 serverMiddleware,
220];
221```
222
223Client middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware differs from server middleware because there's no HTTP Request, so it doesn't have a `Response` to bubble up. In most cases, you can just ignore the return value from `next` and return nothing from your middleware on the client:
224
225```ts
226async function clientMiddleware({ request }, next) {
227 console.log(request.method, request.url);
228 await next();
229 console.log(`Finished ${request.method} ${request.url}`);
230}
231
232// Framework mode
233export const clientMiddleware: Route.ClientMiddlewareFunction[] =
234 [clientMiddleware];
235
236// Or, Data mode
237const route = {
238 path: "/",
239 middleware: [clientMiddleware],
240 loader: rootLoader,
241 Component: Root,
242};
243```
244
245There may be _some_ cases where you want to do some post-processing based on the result of the loaders/action. In lieu of a `Response`, client middleware bubbles up the value returned from the active [`dataStrategy`][datastrategy] (`Record<string, DataStrategyResult>` - keyed by route id). This allows you to take conditional action in your middleware based on the outcome of the executed `loader`/`action` functions.
246
247Here's an example of the [CMS Redirect on 404][cms-redirect] use case implemented as a client side middleware:
248
249```tsx
250async function cmsFallbackMiddleware({ request }, next) {
251 const results = await next();
252
253 // Check if we got a 404 from any of our routes and if so, look for a
254 // redirect in our CMS
255 const found404 = Object.values(results).some(
256 (r) =>
257 isRouteErrorResponse(r.result) &&
258 r.result.status === 404,
259 );
260 if (found404) {
261 const cmsRedirect = await checkCMSRedirects(
262 request.url,
263 );
264 if (cmsRedirect) {
265 throw redirect(cmsRedirect, 302);
266 }
267 }
268}
269```
270
271<docs-warning>In a server middleware, you shouldn't be messing with the `Response` body and should only be reading status/headers and setting headers. Similarly, this value should be considered read-only in client middleware because it represents the "body" or "data" for the resulting navigation which should be driven by loaders/actions - not middleware. This also means that in client middleware, there's usually no need to return the results even if you needed to capture it from `await next()`;</docs-warning>
272
273### When Middleware Runs
274
275It is very important to understand _when_ your middlewares will run to make sure your application is behaving as you intend.
276
277#### Server Middleware
278
279In 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.
280
281This raises the question of what is a "handler" in React Router? Is it the route? Or the `loader`? We think "it depends":
282
283- On document requests (`GET /route`), the handler is the route — because the response encompasses both the `loader` and the route component
284- On data requests (`GET /route.data`) for client-side navigations, the handler is the [`action`][data-action]/[`loader`][data-loader], because that's all that is included in the response
285
286Therefore:
287
288- Document requests run server middleware whether `loader`s exist or not because we're still in a "handler" to render the UI
289- Client-side navigations will only run server middleware if a `.data` request is made to the server for a [`action`][framework-action]/[`loader`][framework-loader]
290
291This 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.
292
293```tsx filename=app/root.tsx
294// This middleware won't run on client-side navigations without a `.data` request
295async function loggingMiddleware({ request }, next) {
296 console.log(`Request: ${request.method} ${request.url}`);
297 let response = await next();
298 console.log(
299 `Response: ${response.status} ${request.method} ${request.url}`,
300 );
301 return response;
302}
303
304export const middleware: Route.MiddlewareFunction[] = [
305 loggingMiddleware,
306];
307```
308
309However, 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 than when they submit to the `action`. If your middleware meets these 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.
310
311```tsx filename=app/_auth.tsx
312function authMiddleware({ request }, next) {
313 if (!isLoggedIn(request)) {
314 throw redirect("/login");
315 }
316}
317
318export const middleware: Route.MiddlewareFunction[] = [
319 authMiddleware,
320];
321
322// By adding a `loader`, we force the `authMiddleware` to run on every
323// client-side navigation involving this route.
324export async function loader() {
325 return null;
326}
327```
328
329#### Client Middleware
330
331Client 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 there are `loader`s to run.
332
333### Context API
334
335The context system provides type safety, prevents naming conflicts, and allows you to provide data to nested middlewares and `action`/`loader` functions.
336
337```ts
338// ✅ Type-safe
339import { createContext } from "react-router";
340const userContext = createContext<User>();
341
342// Later in middleware/`loader`s
343context.set(userContext, user); // Must be `User` type
344const user = context.get(userContext); // Returns `User` type
345
346// ❌ Old way (no type safety)
347context.user = user; // Could be anything
348```
349
350#### `Context` and `AsyncLocalStorage`
351
352Node provides an [`AsyncLocalStorage`][asynclocalstorage] API which gives you a way to provide values through asynchronous execution contexts. While this is a Node API, most modern runtimes have made it (mostly) available (i.e., [Cloudflare][cloudflare], [Bun][bun], [Deno][deno]).
353
354In theory, we could have leveraged [`AsyncLocalStorage`][asynclocalstorage] directly as the way to pass values from middlewares 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 a way to publish reusable middleware packages guaranteed to work in a runtime-agnostic manner.
355
356That said, this API still works great with React Router middleware and can be used in place of, or alongside of the `context` API:
357
358<docs-info>[`AsyncLocalStorage`][asynclocalstorage] is _especially_ powerful when using [React Server Components](../how-to/react-server-components) because it allows you to provide information from `middleware` to your Server Components and route actions because they run in the same server execution context 🤯</docs-info>
359
360<docs-warning>Do not rely on route middleware to provide access control for React Server Functions. Server Functions are not inherently associated with a route, and a client can call the same Server Function through a URL with different middleware. Server Functions must perform all of their own access control checks. Use a route `action` instead when you want middleware-driven access control.</docs-warning>
361
362```tsx filename=app/user-context.ts
363import { AsyncLocalStorage } from "node:async_hooks";
364
365const USER = new AsyncLocalStorage<User>();
366
367export async function provideUser(
368 request: Request,
369 cb: () => Promise<Response>,
370) {
371 let user = await getUser(request);
372 return USER.run(user, cb);
373}
374
375export function getUser() {
376 return USER.getStore();
377}
378```
379
380```tsx filename=app/root.tsx
381import { provideUser } from "./user-context";
382
383export const middleware: Route.MiddlewareFunction[] = [
384 async ({ request, context }, next) => {
385 return provideUser(request, async () => {
386 let res = await next();
387 return res;
388 });
389 },
390];
391```
392
393```tsx filename=app/routes/_index.tsx
394import { getUser } from "../user-context";
395
396export async function loader() {
397 let user = getUser();
398 //...
399}
400```
401
402### The `next` function
403
404The `next` function logic depends on which route middleware it's being called from:
405
406- When called from a non-leaf middleware, it runs the next middleware in the chain
407- When called from the leaf middleware, it executes any route handlers and generates the resulting [`Response`][Response] for the request
408
409```ts
410const middleware = async ({ context }, next) => {
411 // Code here runs BEFORE handlers
412 console.log("Before");
413
414 const response = await next();
415
416 // Code here runs AFTER handlers
417 console.log("After");
418
419 return response; // Optional on client, required on server
420};
421```
422
423<docs-warning>You can only call `next()` once per middleware. Calling it multiple times will throw an error</docs-warning>
424
425### Skipping `next()`
426
427If you don't need to run code after your handlers, you can skip calling `next()`:
428
429```ts
430const authMiddleware = async ({ request, context }) => {
431 const user = await getUser(request);
432 if (!user) {
433 throw redirect("/login");
434 }
435 context.set(userContext, user);
436 // next() is called automatically
437};
438```
439
440### `next()` and Error Handling
441
442React Router contains built-in error handling via the route [`ErrorBoundary`][ErrorBoundary] export. Just like when a `action`/`loader` throws, if a `middleware` throws it will be caught and handled at the appropriate [`ErrorBoundary`][ErrorBoundary] and a [`Response`][Response] will be returned through the ancestor `next()` call. This means that the `next()` function should never throw and should always return a [`Response`][Response], so you don't need to worry about wrapping it in a try/catch.
443
444This 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.
445
446```tsx filename=routes/parent.tsx
447export const middleware: Route.MiddlewareFunction[] = [
448 async (_, next) => {
449 let res = await next();
450 // ^ res.status = 500
451 // This response contains the ErrorBoundary
452 return res;
453 },
454];
455```
456
457```tsx filename=routes/parent.child.tsx
458export const middleware: Route.MiddlewareFunction[] = [
459 async (_, next) => {
460 let res = await next();
461 // ^ res.status = 200
462 // This response contains the successful UI render
463 throw new Error("Uh oh, something went wrong!");
464 },
465];
466```
467
468Which `ErrorBoundary` is rendered will differ based on whether your middleware threw _before_ or _after_ calling then `next()` function. If it throws _after_ then it will bubble up from the throwing route just like a normal loader error because we've already run the loaders and have the appropriate `loaderData` to render in the route components. However, if an error is thrown _before_ calling `next()`, then we haven't called any loaders yet and there is no `loaderData` available. When this happens, we must bubble up to the highest route with a `loader` and start looking for an `ErrorBoundary` there. We cannot render any route components at that level or below without any `loaderData`.
469
470## Common Patterns
471
472### Authentication
473
474Middleware on a protected route or a common ancestor can authenticate and authorize a request before the matched loaders and actions run.
475
476Loaders run in parallel and can be requested independently, so parent loaders are not capable of "protecting" child route loaders or actions. Each protected loader and action must perform its own checks unless appropriate server middleware enforces them before the handler runs.
477
478<docs-warning>Attach authorization middleware to the protected route or layout instead of gating its checks with literal pathname-prefix comparisons such as `url.pathname.startsWith("/admin")`. Literal checks may miss potential URL encodings that React Router route matching would cover.</docs-warning>
479
480```tsx filename=app/middleware/auth.ts
481import { redirect } from "react-router";
482import { userContext } from "~/context";
483import { getSession } from "~/sessions.server";
484
485export const authMiddleware = async ({
486 request,
487 context,
488}) => {
489 const session = await getSession(request);
490 const userId = session.get("userId");
491
492 if (!userId) {
493 throw redirect("/login");
494 }
495
496 const user = await getUserById(userId);
497 context.set(userContext, user);
498};
499```
500
501```tsx filename=app/routes/protected.tsx
502import { authMiddleware } from "~/middleware/auth";
503
504export const middleware: Route.MiddlewareFunction[] = [
505 authMiddleware,
506];
507
508export async function loader({
509 context,
510}: Route.LoaderArgs) {
511 const user = context.get(userContext); // Guaranteed to exist
512 return { user };
513}
514```
515
516### Logging
517
518```tsx filename=app/middleware/logging.ts
519import { requestIdContext } from "~/context";
520
521export const loggingMiddleware = async (
522 { request, context },
523 next,
524) => {
525 const requestId = crypto.randomUUID();
526 context.set(requestIdContext, requestId);
527
528 console.log(
529 `[${requestId}] ${request.method} ${request.url}`,
530 );
531
532 const start = performance.now();
533 const response = await next();
534 const duration = performance.now() - start;
535
536 console.log(
537 `[${requestId}] Response ${response.status} (${duration}ms)`,
538 );
539
540 return response;
541};
542```
543
544### CMS Redirect on 404
545
546```tsx filename=app/middleware/cms-fallback.ts
547export const cmsFallbackMiddleware = async (
548 { request },
549 next,
550) => {
551 const response = await next();
552
553 // Check if we got a 404
554 if (response.status === 404) {
555 // Check CMS for a redirect
556 const cmsRedirect = await checkCMSRedirects(
557 request.url,
558 );
559 if (cmsRedirect) {
560 throw redirect(cmsRedirect, 302);
561 }
562 }
563
564 return response;
565};
566```
567
568### Response Headers
569
570```tsx filename=app/middleware/headers.ts
571export const headersMiddleware = async (
572 { context },
573 next,
574) => {
575 const response = await next();
576
577 // Add security headers
578 response.headers.set("X-Frame-Options", "DENY");
579 response.headers.set("X-Content-Type-Options", "nosniff");
580
581 return response;
582};
583```
584
585### Conditional Middleware
586
587```tsx
588export const middleware: Route.MiddlewareFunction[] = [
589 async ({ request, context }, next) => {
590 // Only run auth for POST requests
591 if (request.method === "POST") {
592 await ensureAuthenticated(request, context);
593 }
594 return next();
595 },
596];
597```
598
599### Sharing Context Between `action` and `loader`
600
601<docs-info>On the server, this approach only works for document POST requests because `context` is scoped to a request. SPA navigation submissions use separate POST/GET requests so you cannot share `context` between them. This pattern always works in `clientMiddleware`/`clientLoader`/`clientAction` because there's no separate HTTP requests.</docs-info>
602
603```tsx
604const sharedDataContext = createContext<any>();
605
606export const middleware: Route.MiddlewareFunction[] = [
607 async ({ request, context }, next) => {
608 // Set data if it doesn't exist
609 // This will only run once for document requests
610 // It will run twice (action request + loader request) in SPA submissions
611 if (!context.get(sharedDataContext)) {
612 context.set(
613 sharedDataContext,
614 await getExpensiveData(),
615 );
616 }
617 return next();
618 },
619];
620
621export async function action({
622 context,
623}: Route.ActionArgs) {
624 const data = context.get(sharedDataContext);
625 // Use the data...
626}
627
628export async function loader({
629 context,
630}: Route.LoaderArgs) {
631 const data = context.get(sharedDataContext);
632 // Same data is available here
633}
634```
635
636[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
637[common-patterns]: #common-patterns
638[server-client]: #server-vs-client-middleware
639[framework-action]: ../start/framework/route-module#action
640[framework-loader]: ../start/framework/route-module#loader
641[getloadcontext]: ../api/other-api/adapter#react-routerexpress
642[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
643[cms-redirect]: #cms-redirect-on-404
644[createContext]: ../api/utils/createContext
645[getContext]: ../api/data-routers/createBrowserRouter#optsgetContext
646[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request
647[data-action]: ../start/data/route-object#action
648[data-loader]: ../start/data/route-object#loader
649[asynclocalstorage]: https://nodejs.org/api/async_context.html#class-asynclocalstorage
650[cloudflare]: https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/
651[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support
652[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage
653[ErrorBoundary]: ../start/framework/route-module#errorboundary