UNPKG

27.7 kB Markdown View 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. Enable the middleware flag
33
34First, enable middleware in your [React Router config][rr-config]:
35
36```ts filename=react-router.config.ts
37import type { Config } from "@react-router/dev/config";
38
39export default {
40 future: {
41 v8_middleware: true,
42 },
43} satisfies Config;
44```
45
46<docs-warning>By enabling the middleware feature, you change the type of the `context` parameter to your [`action`][framework-action]s and [`loader`][framework-loader]s. Please pay attention to the section on [`getLoadContext`][getloadcontext] below if you are actively using `context` today.</docs-warning>
47
48### 2. Create a context
49
50Middleware uses a `context` provider instance to provide data down the middleware chain.
51You can create type-safe context objects using [`createContext`][createContext]:
52
53```ts filename=app/context.ts
54import { createContext } from "react-router";
55import type { User } from "~/types";
56
57export const userContext = createContext<User | null>(null);
58```
59
60### 3. Export middleware from your routes
61
62```tsx filename=app/routes/dashboard.tsx
63import { redirect } from "react-router";
64import { userContext } from "~/context";
65
66// Server-side Authentication Middleware
67async function authMiddleware({ request, context }) {
68 const user = await getUserFromSession(request);
69 if (!user) {
70 throw redirect("/login");
71 }
72 context.set(userContext, user);
73}
74
75export const middleware: Route.MiddlewareFunction[] = [
76 authMiddleware,
77];
78
79// Client-side timing middleware
80async function timingMiddleware({ context }, next) {
81 const start = performance.now();
82 await next();
83 const duration = performance.now() - start;
84 console.log(`Navigation took ${duration}ms`);
85}
86
87export const clientMiddleware: Route.ClientMiddlewareFunction[] =
88 [timingMiddleware];
89
90export async function loader({
91 context,
92}: Route.LoaderArgs) {
93 const user = context.get(userContext);
94 const profile = await getProfile(user);
95 return { profile };
96}
97
98export default function Dashboard({
99 loaderData,
100}: Route.ComponentProps) {
101 return (
102 <div>
103 <h1>Welcome {loaderData.profile.fullName}!</h1>
104 <Profile profile={loaderData.profile} />
105 </div>
106 );
107}
108```
109
110### 4. Update your `getLoadContext` function (if applicable)
111
112If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of [`RouterContextProvider`][RouterContextProvider], instead of a JavaScript object:
113
114```diff
115+import {
116+ createContext,
117+ RouterContextProvider,
118+} from "react-router";
119import { createDb } from "./db";
120
121+const dbContext = createContext<Database>();
122
123function getLoadContext(req, res) {
124- return { db: createDb() };
125+ const context = new RouterContextProvider();
126+ context.set(dbContext, createDb());
127+ return context;
128}
129```
130
131## Quick Start (Data Mode)
132
133### 1. TypeScript: augment `Future` for loader/action `context`
134
135In order to properly type your `context` param in your `loader`/`action`/`middleware` functions, you will need a small module augmentation to override the default context type of `any`:
136
137```ts
138// src/react-router.d.ts
139
140import "react-router";
141
142declare module "react-router" {
143 interface Future {
144 v8_middleware: true;
145 }
146}
147```
148
149Without this, `context` stays loosely typed even when middleware is enabled at runtime.
150
151### 2. Create a context
152
153Middleware uses a `context` provider to pass data through the middleware chain into loaders and actions. Create typed context with [`createContext`][createContext]:
154
155```ts
156import { createContext } from "react-router";
157import type { User } from "~/types";
158
159export const userContext = createContext<User | null>(null);
160```
161
162### 3. Add `middleware` to route objects
163
164Attach `middleware` arrays to your route objects:
165
166```tsx
167import {
168 redirect,
169 useLoaderData,
170 type LoaderFunctionArgs,
171} from "react-router";
172import { userContext } from "~/context";
173
174const routes = [
175 {
176 path: "/",
177 middleware: [timingMiddleware], // 👈
178 Component: Root,
179 children: [
180 {
181 path: "dashboard",
182 middleware: [authMiddleware], // 👈
183 loader: dashboardLoader,
184 Component: Dashboard,
185 },
186 {
187 path: "login",
188 Component: Login,
189 },
190 ],
191 },
192];
193
194async function timingMiddleware({ context }, next) {
195 const start = performance.now();
196 await next();
197 const duration = performance.now() - start;
198 console.log(`Navigation took ${duration}ms`);
199}
200
201async function authMiddleware({ context }) {
202 const user = await getUser();
203 if (!user) {
204 throw redirect("/login");
205 }
206 context.set(userContext, user);
207}
208
209export async function dashboardLoader({
210 context,
211}: LoaderFunctionArgs) {
212 const user = context.get(userContext);
213 const profile = await getProfile(user);
214 return { profile };
215}
216
217export default function Dashboard() {
218 let loaderData = useLoaderData();
219 return (
220 <div>
221 <h1>Welcome {loaderData.profile.fullName}!</h1>
222 <Profile profile={loaderData.profile} />
223 </div>
224 );
225}
226```
227
228### 4. Add a `getContext` function (optional)
229
230To seed every navigation or fetcher call with shared values, pass [`getContext`][getContext] when creating the router:
231
232```tsx
233let sessionContext = createContext();
234
235const router = createBrowserRouter(routes, {
236 getContext() {
237 let context = new RouterContextProvider();
238 context.set(sessionContext, getSession());
239 return context;
240 },
241});
242```
243
244<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>
245
246## Core Concepts
247
248### Server vs Client Middleware
249
250Server 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:
251
252```ts
253async function serverMiddleware({ request }, next) {
254 console.log(request.method, request.url);
255 let response = await next();
256 console.log(response.status, request.method, request.url);
257 return response;
258}
259
260// Framework mode only
261export const middleware: Route.MiddlewareFunction[] = [
262 serverMiddleware,
263];
264```
265
266Client 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:
267
268```ts
269async function clientMiddleware({ request }, next) {
270 console.log(request.method, request.url);
271 await next();
272 console.log(`Finished ${request.method} ${request.url}`);
273}
274
275// Framework mode
276export const clientMiddleware: Route.ClientMiddlewareFunction[] =
277 [clientMiddleware];
278
279// Or, Data mode
280const route = {
281 path: "/",
282 middleware: [clientMiddleware],
283 loader: rootLoader,
284 Component: Root,
285};
286```
287
288There 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.
289
290Here's an example of the [CMS Redirect on 404][cms-redirect] use case implemented as a client side middleware:
291
292```tsx
293async function cmsFallbackMiddleware({ request }, next) {
294 const results = await next();
295
296 // Check if we got a 404 from any of our routes and if so, look for a
297 // redirect in our CMS
298 const found404 = Object.values(results).some(
299 (r) =>
300 isRouteErrorResponse(r.result) &&
301 r.result.status === 404,
302 );
303 if (found404) {
304 const cmsRedirect = await checkCMSRedirects(
305 request.url,
306 );
307 if (cmsRedirect) {
308 throw redirect(cmsRedirect, 302);
309 }
310 }
311}
312```
313
314<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>
315
316### When Middleware Runs
317
318It is very important to understand _when_ your middlewares will run to make sure your application is behaving as you intend.
319
320#### Server Middleware
321
322In 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.
323
324This raises the question of what is a "handler" in React Router? Is it the route? Or the `loader`? We think "it depends":
325
326- On document requests (`GET /route`), the handler is the route — because the response encompasses both the `loader` and the route component
327- 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
328
329Therefore:
330
331- Document requests run server middleware whether `loader`s exist or not because we're still in a "handler" to render the UI
332- 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]
333
334This 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.
335
336```tsx filename=app/root.tsx
337// This middleware won't run on client-side navigations without a `.data` request
338async function loggingMiddleware({ request }, next) {
339 console.log(`Request: ${request.method} ${request.url}`);
340 let response = await next();
341 console.log(
342 `Response: ${response.status} ${request.method} ${request.url}`,
343 );
344 return response;
345}
346
347export const middleware: Route.MiddlewareFunction[] = [
348 loggingMiddleware,
349];
350```
351
352However, 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.
353
354```tsx filename=app/_auth.tsx
355function authMiddleware({ request }, next) {
356 if (!isLoggedIn(request)) {
357 throw redirect("/login");
358 }
359}
360
361export const middleware: Route.MiddlewareFunction[] = [
362 authMiddleware,
363];
364
365// By adding a `loader`, we force the `authMiddleware` to run on every
366// client-side navigation involving this route.
367export async function loader() {
368 return null;
369}
370```
371
372#### Client Middleware
373
374Client 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.
375
376### Context API
377
378The 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.
379
380```ts
381// ✅ Type-safe
382import { createContext } from "react-router";
383const userContext = createContext<User>();
384
385// Later in middleware/`loader`s
386context.set(userContext, user); // Must be `User` type
387const user = context.get(userContext); // Returns `User` type
388
389// ❌ Old way (no type safety)
390context.user = user; // Could be anything
391```
392
393#### `Context` and `AsyncLocalStorage`
394
395Node 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]).
396
397In 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.
398
399That said, this API still works great with React Router middleware and can be used in place of, or alongside of the `context` API:
400
401<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>
402
403```tsx filename=app/user-context.ts
404import { AsyncLocalStorage } from "node:async_hooks";
405
406const USER = new AsyncLocalStorage<User>();
407
408export async function provideUser(
409 request: Request,
410 cb: () => Promise<Response>,
411) {
412 let user = await getUser(request);
413 return USER.run(user, cb);
414}
415
416export function getUser() {
417 return USER.getStore();
418}
419```
420
421```tsx filename=app/root.tsx
422import { provideUser } from "./user-context";
423
424export const middleware: Route.MiddlewareFunction[] = [
425 async ({ request, context }, next) => {
426 return provideUser(request, async () => {
427 let res = await next();
428 return res;
429 });
430 },
431];
432```
433
434```tsx filename=app/routes/_index.tsx
435import { getUser } from "../user-context";
436
437export async function loader() {
438 let user = getUser();
439 //...
440}
441```
442
443### The `next` function
444
445The `next` function logic depends on which route middleware it's being called from:
446
447- When called from a non-leaf middleware, it runs the next middleware in the chain
448- When called from the leaf middleware, it executes any route handlers and generates the resulting [`Response`][Response] for the request
449
450```ts
451const middleware = async ({ context }, next) => {
452 // Code here runs BEFORE handlers
453 console.log("Before");
454
455 const response = await next();
456
457 // Code here runs AFTER handlers
458 console.log("After");
459
460 return response; // Optional on client, required on server
461};
462```
463
464<docs-warning>You can only call `next()` once per middleware. Calling it multiple times will throw an error</docs-warning>
465
466### Skipping `next()`
467
468If you don't need to run code after your handlers, you can skip calling `next()`:
469
470```ts
471const authMiddleware = async ({ request, context }) => {
472 const user = await getUser(request);
473 if (!user) {
474 throw redirect("/login");
475 }
476 context.set(userContext, user);
477 // next() is called automatically
478};
479```
480
481### `next()` and Error Handling
482
483React 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.
484
485This 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.
486
487```tsx filename=routes/parent.tsx
488export const middleware: Route.MiddlewareFunction[] = [
489 async (_, next) => {
490 let res = await next();
491 // ^ res.status = 500
492 // This response contains the ErrorBoundary
493 return res;
494 },
495];
496```
497
498```tsx filename=routes/parent.child.tsx
499export const middleware: Route.MiddlewareFunction[] = [
500 async (_, next) => {
501 let res = await next();
502 // ^ res.status = 200
503 // This response contains the successful UI render
504 throw new Error("Uh oh, something went wrong!");
505 },
506];
507```
508
509Which `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`.
510
511## Changes to `getLoadContext`/`AppLoadContext`
512
513<docs-info>This only applies if you are using a custom server and a custom `getLoadContext` function</docs-info>
514
515Middleware introduces a breaking change to the `context` parameter generated by `getLoadContext` and passed to your `action`s and `loader`s. The current approach of a module-augmented `AppLoadContext` isn't really type-safe and instead just sort of tells TypeScript to "trust me".
516
517Middleware 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.
518
519When opting into middleware, the `context` parameter changes to an instance of [`RouterContextProvider`][RouterContextProvider]:
520
521```ts
522let dbContext = createContext<Database>();
523let context = new RouterContextProvider();
524context.set(dbContext, getDb());
525// ^ type-safe
526let db = context.get(dbContext);
527// ^ Database
528```
529
530If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of [`RouterContextProvider`][RouterContextProvider], instead of a plain JavaScript object:
531
532```diff
533+import {
534+ createContext,
535+ RouterContextProvider,
536+} from "react-router";
537import { createDb } from "./db";
538
539+const dbContext = createContext<Database>();
540
541function getLoadContext(req, res) {
542- return { db: createDb() };
543+ const context = new RouterContextProvider();
544+ context.set(dbContext, createDb());
545+ return context;
546}
547```
548
549### Migration from `AppLoadContext`
550
551If you're currently using `AppLoadContext`, you can migrate incrementally by using your existing module augmentation to augment [`RouterContextProvider`][RouterContextProvider] instead of `AppLoadContext`. Then, update your `getLoadContext` function to return an instance of [`RouterContextProvider`][RouterContextProvider]:
552
553```diff
554declare module "react-router" {
555- interface AppLoadContext {
556+ interface RouterContextProvider {
557 db: Database;
558 user: User;
559 }
560}
561
562function getLoadContext() {
563 const loadContext = {...};
564- return loadContext;
565+ let context = new RouterContextProvider();
566+ Object.assign(context, loadContext);
567+ return context;
568}
569```
570
571This allows you to leave your `action`s/`loader`s untouched during initial adoption of middleware, since they can still read values directly (i.e., `context.db`).
572
573<docs-warning>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.</docs-warning>
574
575<docs-warning>The [`RouterContextProvider`][RouterContextProvider] class is also used for the client-side `context` parameter via `<HydratedRouter getContext>` and `<RouterProvider 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 `getContext` on the client).</docs-warning>
576
577## Common Patterns
578
579### Authentication
580
581```tsx filename=app/middleware/auth.ts
582import { redirect } from "react-router";
583import { userContext } from "~/context";
584import { getSession } from "~/sessions.server";
585
586export const authMiddleware = async ({
587 request,
588 context,
589}) => {
590 const session = await getSession(request);
591 const userId = session.get("userId");
592
593 if (!userId) {
594 throw redirect("/login");
595 }
596
597 const user = await getUserById(userId);
598 context.set(userContext, user);
599};
600```
601
602```tsx filename=app/routes/protected.tsx
603import { authMiddleware } from "~/middleware/auth";
604
605export const middleware: Route.MiddlewareFunction[] = [
606 authMiddleware,
607];
608
609export async function loader({
610 context,
611}: Route.LoaderArgs) {
612 const user = context.get(userContext); // Guaranteed to exist
613 return { user };
614}
615```
616
617### Logging
618
619```tsx filename=app/middleware/logging.ts
620import { requestIdContext } from "~/context";
621
622export const loggingMiddleware = async (
623 { request, context },
624 next,
625) => {
626 const requestId = crypto.randomUUID();
627 context.set(requestIdContext, requestId);
628
629 console.log(
630 `[${requestId}] ${request.method} ${request.url}`,
631 );
632
633 const start = performance.now();
634 const response = await next();
635 const duration = performance.now() - start;
636
637 console.log(
638 `[${requestId}] Response ${response.status} (${duration}ms)`,
639 );
640
641 return response;
642};
643```
644
645### CMS Redirect on 404
646
647```tsx filename=app/middleware/cms-fallback.ts
648export const cmsFallbackMiddleware = async (
649 { request },
650 next,
651) => {
652 const response = await next();
653
654 // Check if we got a 404
655 if (response.status === 404) {
656 // Check CMS for a redirect
657 const cmsRedirect = await checkCMSRedirects(
658 request.url,
659 );
660 if (cmsRedirect) {
661 throw redirect(cmsRedirect, 302);
662 }
663 }
664
665 return response;
666};
667```
668
669### Response Headers
670
671```tsx filename=app/middleware/headers.ts
672export const headersMiddleware = async (
673 { context },
674 next,
675) => {
676 const response = await next();
677
678 // Add security headers
679 response.headers.set("X-Frame-Options", "DENY");
680 response.headers.set("X-Content-Type-Options", "nosniff");
681
682 return response;
683};
684```
685
686### Conditional Middleware
687
688```tsx
689export const middleware: Route.MiddlewareFunction[] = [
690 async ({ request, context }, next) => {
691 // Only run auth for POST requests
692 if (request.method === "POST") {
693 await ensureAuthenticated(request, context);
694 }
695 return next();
696 },
697];
698```
699
700### Sharing Context Between `action` and `loader`
701
702<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>
703
704```tsx
705const sharedDataContext = createContext<any>();
706
707export const middleware: Route.MiddlewareFunction[] = [
708 async ({ request, context }, next) => {
709 // Set data if it doesn't exist
710 // This will only run once for document requests
711 // It will run twice (action request + loader request) in SPA submissions
712 if (!context.get(sharedDataContext)) {
713 context.set(
714 sharedDataContext,
715 await getExpensiveData(),
716 );
717 }
718 return next();
719 },
720];
721
722export async function action({
723 context,
724}: Route.ActionArgs) {
725 const data = context.get(sharedDataContext);
726 // Use the data...
727}
728
729export async function loader({
730 context,
731}: Route.LoaderArgs) {
732 const data = context.get(sharedDataContext);
733 // Same data is available here
734}
735```
736
737[future-flags]: ../upgrading/future
738[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
739[common-patterns]: #common-patterns
740[server-client]: #server-vs-client-middleware
741[rr-config]: ../api/framework-conventions/react-router.config.ts
742[create-browser-router]: ../api/data-routers/createBrowserRouter
743[create-hash-router]: ../api/data-routers/createHashRouter
744[create-memory-router]: ../api/data-routers/createMemoryRouter
745[create-static-handler]: ../api/data-routers/createStaticHandler
746[framework-action]: ../start/framework/route-module#action
747[framework-loader]: ../start/framework/route-module#loader
748[getloadcontext]: #changes-to-getloadcontextapploadcontext
749[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
750[cms-redirect]: #cms-redirect-on-404
751[createContext]: ../api/utils/createContext
752[RouterContextProvider]: ../api/utils/RouterContextProvider
753[getContext]: ../api/data-routers/createBrowserRouter#optsgetContext
754[window]: https://developer.mozilla.org/en-US/docs/Web/API/Window
755[document]: https://developer.mozilla.org/en-US/docs/Web/API/Document
756[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request
757[data-action]: ../start/data/route-object#action
758[data-loader]: ../start/data/route-object#loader
759[asynclocalstorage]: https://nodejs.org/api/async_context.html#class-asynclocalstorage
760[cloudflare]: https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/
761[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support
762[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage
763[ErrorBoundary]: ../start/framework/route-module#errorboundary