Skip to content

Commit 88beda8

Browse files
committed
Update middleware docs
1 parent 86a3dd5 commit 88beda8

File tree

1 file changed

+139
-113
lines changed

1 file changed

+139
-113
lines changed

docs/how-to/middleware.md

Lines changed: 139 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ unstable: true
77

88
<docs-warning>The middleware feature is currently experimental and subject to breaking changes. Use the `future.unstable_middleware` flag to enable it.</docs-warning>
99

10-
Middleware allows you to run code before and after your route handlers (loaders, actions, and components) execute. This enables common patterns like authentication, logging, error handling, and data preprocessing in a reusable way.
10+
Middleware allows you to run code before and after the `Response` generation for the matched path. This enables common patterns like authentication, logging, error handling, and data preprocessing in a reusable way.
1111

12-
Middleware runs in a nested chain, executing from parent routes to child routes on the way "down" to your route handlers, then from child routes back to parent routes on the way "up" after your handlers complete.
12+
Middleware runs in a nested chain, executing from parent routes to child routes on the way "down" to your route handlers, then from child routes back to parent routes on the way "up" after a `Response` is generated.
1313

1414
For example, on a `GET /parent/child` request, the middleware would run in the following order:
1515

1616
```text
1717
- Root middleware start
1818
- Parent middleware start
1919
- Child middleware start
20-
- Run loaders
20+
- Run loaders, generate HTML Response
2121
- Child middleware end
2222
- Parent middleware end
2323
- Root middleware end
@@ -39,11 +39,12 @@ export default {
3939
} satisfies Config;
4040
```
4141

42-
<docs-warning>By enabling the middleware feature, you change the type of the `context` parameter to your loaders and actions. Please pay attention to the section on [getLoadContext](#custom-server-with-getloadcontext) below if you are actively using `context` today.</docs-warning>
42+
<docs-warning>By enabling the middleware feature, you change the type of the `context` parameter to your loaders and actions. Please pay attention to the section on [getLoadContext](#changes-to-getloadcontextapploadcontext) below if you are actively using `context` today.</docs-warning>
4343

4444
### 2. Create a context
4545

46-
Create type-safe context objects using `unstable_createContext`:
46+
Middleware uses a `context` provider instance to provide data down the middleware chain.
47+
You can create type-safe context objects using `unstable_createContext`:
4748

4849
```ts filename=app/context.ts
4950
import { unstable_createContext } from "react-router";
@@ -104,6 +105,22 @@ export default function Dashboard({
104105
}
105106
```
106107

108+
#### 4. Update your `getLoadContext` function (if applicable)
109+
110+
If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return a Map of contexts and values, instead of a JavaScript object:
111+
112+
```diff
113+
+import { unstable_createContext } from "react-router";
114+
import { createDb } from "./db";
115+
+
116+
+const dbContext = unstable_createContext<Database>();
117+
118+
function getLoadContext(req, res) {
119+
- return { db: createDb() };
120+
+ const map = new Map([[dbContext, createDb()]]);
121+
}
122+
```
123+
107124
## Core Concepts
108125

109126
### Server vs Client Middleware
@@ -117,9 +134,29 @@ export default function Dashboard({
117134

118135
- Client-side navigations and fetcher calls
119136

137+
### Context API
138+
139+
The new context system provides type safety and prevents naming conflicts:
140+
141+
```ts
142+
// ✅ Type-safe
143+
import { unstable_createContext } from "react-router";
144+
const userContext = unstable_createContext<User>();
145+
146+
// Later in middleware/loaders
147+
context.set(userContext, user); // Must be User type
148+
const user = context.get(userContext); // Returns User type
149+
150+
// ❌ Old way (no type safety)
151+
// context.user = user; // Could be anything
152+
```
153+
120154
### The `next` Function
121155

122-
The `next` function runs the next middleware in the chain, or the route handlers if it's the leaf route middleware:
156+
The `next` function logic depends on which route middleware it's being called from:
157+
158+
- When called from a non-leaf middleware, it runs the next middleware in the chain
159+
- When called from the leaf middleware, it executes any route handlers and generates the resulting `Response` for the request
123160

124161
```ts
125162
const middleware = async ({ context }, next) => {
@@ -152,21 +189,87 @@ const authMiddleware = async ({ request, context }) => {
152189
};
153190
```
154191

155-
### Context API
192+
### `next()` and Error Handling
156193

157-
The new context system provides type safety and prevents naming conflicts:
194+
`next()` is not designed to throw errors under normal conditions, so you generally shouldn't find yourself wrapping `next` in a `try`/`catch`. The responsibility of the `next()` function is to return a `Response` for the current `Request`, so as long as that can be completed, `next()` will return the Response and won't `throw`. Even if a `loader` throws an error, or a component fails to render, React Router already handles those by rendering the nearest `ErrorBoundary`, so a Response is still generated without issue.
195+
196+
This behavior is important to allow middleware patterns such as automatically setting required headers on outgoing responses (i.e., committing a session) from a root middleware. If any error caused that to throw, we'd miss the execution of ancestor middleware son thew way out and those required headers wouldn't be set.
197+
198+
The only cases in which `next()` _should_ throw are if we fail to generate a Response. There's a few ways in which this could happen:
199+
200+
- A middleware can short circuit the rest of the request and throw a `Response` (usually a `redirect`)
201+
- If the logic directly inside of a middleware function throws, that will cause the ancestor `next()` function to throw
202+
203+
## Changes to `getLoadContext`/`AppLoadContext`
204+
205+
<docs-info>This only applies if you are using a custom server and a custom `getLoadContext` function</docs-info>
206+
207+
Middleware introduces a breaking change to the `context` parameter generated by `getLoadContext` and passed to your loaders and actions. The current approach of a module-augmented `AppLoadContext` isn't really type-safe and instead just sort of tells TypeScript to "trust me".
208+
209+
Middleware needs an equivalent `context` on the client for `clientMiddleware`, but we didn't want to duplicate this pattern from the server that we already weren't thrilled with, so we decided to introduce a new API where we could tackle type-safety.
210+
211+
When opting into middleware, the `context` parameter changes to an instance of `RouterContextProvider`:
158212

159213
```ts
160-
// ✅ Type-safe
214+
let dbContext = unstable_createContext<Database>();
215+
let context = new unstable_RouterContextProvider();
216+
context.set(dbContext, getDb());
217+
// ^ type-safe
218+
let db = context.get(dbContext);
219+
// ^ Database
220+
```
221+
222+
If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return a `Map` of contexts and values, instead of a JavaScript object:
223+
224+
```diff
225+
+import { unstable_createContext } from "react-router";
226+
import { createDb } from "./db";
227+
+
228+
+const dbContext = unstable_createContext<Database>();
229+
230+
function getLoadContext(req, res) {
231+
- return { db: createDb() };
232+
+ const map = new Map([[dbContext, createDb()]]);
233+
}
234+
```
235+
236+
### Migration from `AppLoadContext`
237+
238+
If you're currently using `AppLoadContext`, you can migrate most easily by creating a context for your existing object:
239+
240+
```ts filename=app/context.ts
161241
import { unstable_createContext } from "react-router";
162-
const userContext = unstable_createContext<User>();
163242

164-
// Later in middleware/loaders
165-
context.set(userContext, user); // Must be User type
166-
const user = context.get(userContext); // Returns User type
243+
declare module "@react-router/server-runtime" {
244+
interface AppLoadContext {
245+
db: Database;
246+
user: User;
247+
}
248+
}
167249

168-
// ❌ Old way (no type safety)
169-
// context.user = user; // Could be anything
250+
const myLoadContext =
251+
unstable_createContext<AppLoadContext>();
252+
```
253+
254+
Update your `getLoadContext` function to return a Map with the context initial value:
255+
256+
```diff filename=server.ts
257+
function getLoadContext() {
258+
const loadContext = {...};
259+
- return loadContext;
260+
+ return new Map([
261+
+ [myLoadContext, loadContext]]
262+
+ );
263+
}
264+
```
265+
266+
Update your loaders/actions to read from the new context instance:
267+
268+
```diff filename=app/routes/example.tsx
269+
export function loader({ context }: Route.LoaderArgs) {
270+
- const { db, user } = context;
271+
+ const { db, user } = context.get(myLoadContext);
272+
}
170273
```
171274

172275
## Common Patterns
@@ -233,25 +336,6 @@ export const loggingMiddleware = async (
233336
};
234337
```
235338

236-
### Error Handling
237-
238-
```tsx filename=app/middleware/error-handling.ts
239-
export const errorMiddleware = async (
240-
{ context },
241-
next,
242-
) => {
243-
try {
244-
return await next();
245-
} catch (error) {
246-
// Log error
247-
console.error("Route error:", error);
248-
249-
// Re-throw to let React Router handle it
250-
throw error;
251-
}
252-
};
253-
```
254-
255339
### 404 to CMS Redirect
256340

257341
```tsx filename=app/middleware/cms-fallback.ts
@@ -293,37 +377,6 @@ export const headersMiddleware = async (
293377
};
294378
```
295379

296-
## Client-Side Middleware
297-
298-
Client middleware works similarly but doesn't return responses:
299-
300-
```tsx filename=app/routes/dashboard.tsx
301-
import { userContext } from "~/context";
302-
303-
export const unstable_clientMiddleware = [
304-
({ context }) => {
305-
// Set up client-side user data
306-
const user = getLocalUser();
307-
context.set(userContext, user);
308-
},
309-
310-
async ({ context }, next) => {
311-
console.log("Starting client navigation");
312-
await next();
313-
console.log("Client navigation complete");
314-
},
315-
];
316-
317-
export async function clientLoader({
318-
context,
319-
}: Route.ClientLoaderArgs) {
320-
const user = context.get(userContext);
321-
return { user };
322-
}
323-
```
324-
325-
## Advanced Usage
326-
327380
### Conditional Middleware
328381

329382
```tsx
@@ -371,58 +424,31 @@ export async function loader({
371424
}
372425
```
373426

374-
### Custom Server with getLoadContext
375-
376-
If you're using a custom server, update your `getLoadContext` function:
377-
378-
```ts filename=app/entry.server.tsx
379-
import { unstable_createContext } from "react-router";
380-
import type { unstable_InitialContext } from "react-router";
381-
382-
const dbContext = unstable_createContext<Database>();
383-
384-
function getLoadContext(req, res): unstable_InitialContext {
385-
const map = new Map();
386-
map.set(dbContext, database);
387-
return map;
388-
}
389-
```
390-
391-
### Migration from AppLoadContext
392-
393-
If you're currently using `AppLoadContext`, you can migrate most easily by creating a context for your existing object:
394-
395-
```ts filename=app/context.ts
396-
import { unstable_createContext } from "react-router";
397-
398-
declare module "@react-router/server-runtime" {
399-
interface AppLoadContext {
400-
db: Database;
401-
user: User;
402-
}
403-
}
427+
## Client-Side Middleware
404428

405-
const myLoadContext =
406-
unstable_createContext<AppLoadContext>();
407-
```
429+
Client middleware works similar to server-side middleware but doesn't return responses because it's not running ion response to an HTTP `Request`:
408430

409-
Update your `getLoadContext` function to return a Map with the context initial value:
431+
```tsx filename=app/routes/dashboard.tsx
432+
import { userContext } from "~/context";
410433

411-
```diff filename=app/entry.server.tsx
412-
function getLoadContext() {
413-
const loadContext = {...};
414-
- return loadContext;
415-
+ return new Map([
416-
+ [myLoadContext, appLoadContextInstance]]
417-
+ );
418-
}
419-
```
434+
export const unstable_clientMiddleware = [
435+
({ context }) => {
436+
// Set up client-side user data
437+
const user = getLocalUser();
438+
context.set(userContext, user);
439+
},
420440

421-
Update your loaders/actions to read from the new context instance:
441+
async ({ context }, next) => {
442+
console.log("Starting client navigation");
443+
await next(); // 👈 No return value
444+
console.log("Client navigation complete");
445+
},
446+
];
422447

423-
```diff filename=app/routes/example.tsx
424-
export function loader({ context }: Route.LoaderArgs) {
425-
- const { db, user } = context;
426-
+ const { db, user } = context.get(myLoadContext);
448+
export async function clientLoader({
449+
context,
450+
}: Route.ClientLoaderArgs) {
451+
const user = context.get(userContext);
452+
return { user };
427453
}
428454
```

0 commit comments

Comments
 (0)