UNPKG

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