UNPKG

22.4 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 new context system provides type safety and prevents naming conflicts and allows you to provide data to nested middlewares and `action`/`loader` functions. In Framework Mode, this replaces the previous `AppLoadContext` API.
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 Server Actions because they run in the same server execution context 🤯</docs-info>
362
363```tsx filename=app/user-context.ts
364import { AsyncLocalStorage } from "node:async_hooks";
365
366const USER = new AsyncLocalStorage<User>();
367
368export async function provideUser(
369 request: Request,
370 cb: () => Promise<Response>,
371) {
372 let user = await getUser(request);
373 return USER.run(user, cb);
374}
375
376export function getUser() {
377 return USER.getStore();
378}
379```
380
381```tsx filename=app/root.tsx
382import { provideUser } from "./user-context";
383
384export const middleware: Route.MiddlewareFunction[] = [
385 async ({ request, context }, next) => {
386 return provideUser(request, async () => {
387 let res = await next();
388 return res;
389 });
390 },
391];
392```
393
394```tsx filename=app/routes/_index.tsx
395import { getUser } from "../user-context";
396
397export async function loader() {
398 let user = getUser();
399 //...
400}
401```
402
403### The `next` function
404
405The `next` function logic depends on which route middleware it's being called from:
406
407- When called from a non-leaf middleware, it runs the next middleware in the chain
408- When called from the leaf middleware, it executes any route handlers and generates the resulting [`Response`][Response] for the request
409
410```ts
411const middleware = async ({ context }, next) => {
412 // Code here runs BEFORE handlers
413 console.log("Before");
414
415 const response = await next();
416
417 // Code here runs AFTER handlers
418 console.log("After");
419
420 return response; // Optional on client, required on server
421};
422```
423
424<docs-warning>You can only call `next()` once per middleware. Calling it multiple times will throw an error</docs-warning>
425
426### Skipping `next()`
427
428If you don't need to run code after your handlers, you can skip calling `next()`:
429
430```ts
431const authMiddleware = async ({ request, context }) => {
432 const user = await getUser(request);
433 if (!user) {
434 throw redirect("/login");
435 }
436 context.set(userContext, user);
437 // next() is called automatically
438};
439```
440
441### `next()` and Error Handling
442
443React 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.
444
445This 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.
446
447```tsx filename=routes/parent.tsx
448export const middleware: Route.MiddlewareFunction[] = [
449 async (_, next) => {
450 let res = await next();
451 // ^ res.status = 500
452 // This response contains the ErrorBoundary
453 return res;
454 },
455];
456```
457
458```tsx filename=routes/parent.child.tsx
459export const middleware: Route.MiddlewareFunction[] = [
460 async (_, next) => {
461 let res = await next();
462 // ^ res.status = 200
463 // This response contains the successful UI render
464 throw new Error("Uh oh, something went wrong!");
465 },
466];
467```
468
469Which `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`.
470
471## Common Patterns
472
473### Authentication
474
475```tsx filename=app/middleware/auth.ts
476import { redirect } from "react-router";
477import { userContext } from "~/context";
478import { getSession } from "~/sessions.server";
479
480export const authMiddleware = async ({
481 request,
482 context,
483}) => {
484 const session = await getSession(request);
485 const userId = session.get("userId");
486
487 if (!userId) {
488 throw redirect("/login");
489 }
490
491 const user = await getUserById(userId);
492 context.set(userContext, user);
493};
494```
495
496```tsx filename=app/routes/protected.tsx
497import { authMiddleware } from "~/middleware/auth";
498
499export const middleware: Route.MiddlewareFunction[] = [
500 authMiddleware,
501];
502
503export async function loader({
504 context,
505}: Route.LoaderArgs) {
506 const user = context.get(userContext); // Guaranteed to exist
507 return { user };
508}
509```
510
511### Logging
512
513```tsx filename=app/middleware/logging.ts
514import { requestIdContext } from "~/context";
515
516export const loggingMiddleware = async (
517 { request, context },
518 next,
519) => {
520 const requestId = crypto.randomUUID();
521 context.set(requestIdContext, requestId);
522
523 console.log(
524 `[${requestId}] ${request.method} ${request.url}`,
525 );
526
527 const start = performance.now();
528 const response = await next();
529 const duration = performance.now() - start;
530
531 console.log(
532 `[${requestId}] Response ${response.status} (${duration}ms)`,
533 );
534
535 return response;
536};
537```
538
539### CMS Redirect on 404
540
541```tsx filename=app/middleware/cms-fallback.ts
542export const cmsFallbackMiddleware = async (
543 { request },
544 next,
545) => {
546 const response = await next();
547
548 // Check if we got a 404
549 if (response.status === 404) {
550 // Check CMS for a redirect
551 const cmsRedirect = await checkCMSRedirects(
552 request.url,
553 );
554 if (cmsRedirect) {
555 throw redirect(cmsRedirect, 302);
556 }
557 }
558
559 return response;
560};
561```
562
563### Response Headers
564
565```tsx filename=app/middleware/headers.ts
566export const headersMiddleware = async (
567 { context },
568 next,
569) => {
570 const response = await next();
571
572 // Add security headers
573 response.headers.set("X-Frame-Options", "DENY");
574 response.headers.set("X-Content-Type-Options", "nosniff");
575
576 return response;
577};
578```
579
580### Conditional Middleware
581
582```tsx
583export const middleware: Route.MiddlewareFunction[] = [
584 async ({ request, context }, next) => {
585 // Only run auth for POST requests
586 if (request.method === "POST") {
587 await ensureAuthenticated(request, context);
588 }
589 return next();
590 },
591];
592```
593
594### Sharing Context Between `action` and `loader`
595
596<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>
597
598```tsx
599const sharedDataContext = createContext<any>();
600
601export const middleware: Route.MiddlewareFunction[] = [
602 async ({ request, context }, next) => {
603 // Set data if it doesn't exist
604 // This will only run once for document requests
605 // It will run twice (action request + loader request) in SPA submissions
606 if (!context.get(sharedDataContext)) {
607 context.set(
608 sharedDataContext,
609 await getExpensiveData(),
610 );
611 }
612 return next();
613 },
614];
615
616export async function action({
617 context,
618}: Route.ActionArgs) {
619 const data = context.get(sharedDataContext);
620 // Use the data...
621}
622
623export async function loader({
624 context,
625}: Route.LoaderArgs) {
626 const data = context.get(sharedDataContext);
627 // Same data is available here
628}
629```
630
631[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
632[common-patterns]: #common-patterns
633[server-client]: #server-vs-client-middleware
634[framework-action]: ../start/framework/route-module#action
635[framework-loader]: ../start/framework/route-module#loader
636[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
637[cms-redirect]: #cms-redirect-on-404
638[createContext]: ../api/utils/createContext
639[getContext]: ../api/data-routers/createBrowserRouter#optsgetContext
640[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request
641[data-action]: ../start/data/route-object#action
642[data-loader]: ../start/data/route-object#loader
643[asynclocalstorage]: https://nodejs.org/api/async_context.html#class-asynclocalstorage
644[cloudflare]: https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/
645[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support
646[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage
647[ErrorBoundary]: ../start/framework/route-module#errorboundary