Skip to content

Commit 6661f90

Browse files
authored
Update middleware docs (#14108)
1 parent 5a1786f commit 6661f90

File tree

3 files changed

+388
-64
lines changed

3 files changed

+388
-64
lines changed

docs/how-to/middleware.md

Lines changed: 225 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ unstable: true
55

66
# Middleware
77

8+
[MODES: framework, data]
9+
10+
<br/>
11+
<br/>
12+
813
<docs-warning>The middleware feature is currently experimental and subject to breaking changes. Use the `future.unstable_middleware` flag to enable it.</docs-warning>
914

1015
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.
@@ -23,7 +28,9 @@ For example, on a `GET /parent/child` request, the middleware would run in the f
2328
- Root middleware end
2429
```
2530

26-
## Quick Start
31+
<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>
32+
33+
## Quick Start (Framework mode)
2734

2835
### 1. Enable the middleware flag
2936

@@ -39,7 +46,7 @@ export default {
3946
} satisfies Config;
4047
```
4148

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>
49+
<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][getloadcontext] below if you are actively using `context` today.</docs-warning>
4350

4451
### 2. Create a context
4552

@@ -61,29 +68,27 @@ import { redirect } from "react-router";
6168
import { userContext } from "~/context";
6269

6370
// Server-side Authentication Middleware
71+
async function authMiddleware({ request, context }) {
72+
const user = await getUserFromSession(request);
73+
if (!user) {
74+
throw redirect("/login");
75+
}
76+
context.set(userContext, user);
77+
}
78+
6479
export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
65-
[
66-
async ({ request, context }) => {
67-
const user = await getUserFromSession(request);
68-
if (!user) {
69-
throw redirect("/login");
70-
}
71-
context.set(userContext, user);
72-
},
73-
];
80+
[authMiddleware];
7481

7582
// Client-side timing middleware
76-
export const unstable_clientMiddleware: Route.unstable_ClientMiddlewareFunction[] =
77-
[
78-
async ({ context }, next) => {
79-
const start = performance.now();
80-
81-
await next();
83+
async function timingMiddleware({ context }, next) {
84+
const start = performance.now();
85+
await next();
86+
const duration = performance.now() - start;
87+
console.log(`Navigation took ${duration}ms`);
88+
}
8289

83-
const duration = performance.now() - start;
84-
console.log(`Navigation took ${duration}ms`);
85-
},
86-
];
90+
export const unstable_clientMiddleware: Route.unstable_ClientMiddlewareFunction[] =
91+
[timingMiddleware];
8792

8893
export async function loader({
8994
context,
@@ -105,7 +110,7 @@ export default function Dashboard({
105110
}
106111
```
107112

108-
#### 4. Update your `getLoadContext` function (if applicable)
113+
### 4. Update your `getLoadContext` function (if applicable)
109114

110115
If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of `unstable_RouterContextProvider`, instead of a JavaScript object:
111116

@@ -126,19 +131,211 @@ function getLoadContext(req, res) {
126131
}
127132
```
128133

134+
## Quick Start (Data Mode)
135+
136+
### 1. Enable the middleware flag
137+
138+
```tsx
139+
const router = createBrowserRouter(routes, {
140+
future: {
141+
unstable_middleware: true,
142+
},
143+
});
144+
```
145+
146+
### 2. Create a context
147+
148+
Middleware uses a `context` provider instance to provide data down the middleware chain.
149+
You can create type-safe context objects using `unstable_createContext`:
150+
151+
```ts filename=app/context.ts
152+
import { unstable_createContext } from "react-router";
153+
import type { User } from "~/types";
154+
155+
export const userContext =
156+
unstable_createContext<User | null>(null);
157+
```
158+
159+
### 3. Add middleware to your routes
160+
161+
```tsx
162+
import { redirect } from "react-router";
163+
import { userContext } from "~/context";
164+
165+
const routes = [
166+
{
167+
path: "/",
168+
unstable_middleware: [timingMiddleware], // 👈
169+
Component: Root,
170+
children: [
171+
{
172+
path: "profile",
173+
unstable_middleware: [authMiddleware], // 👈
174+
loader: profileLoader,
175+
Component: Profile,
176+
},
177+
{
178+
path: "login",
179+
Component: Login,
180+
},
181+
],
182+
},
183+
];
184+
185+
async function timingMiddleware({ context }, next) {
186+
const start = performance.now();
187+
await next();
188+
const duration = performance.now() - start;
189+
console.log(`Navigation took ${duration}ms`);
190+
}
191+
192+
async function authMiddleware({ context }) {
193+
const user = await getUser();
194+
if (!user) {
195+
throw redirect("/login");
196+
}
197+
context.set(userContext, user);
198+
}
199+
200+
export async function profileLoader({
201+
context,
202+
}: Route.LoaderArgs) {
203+
const user = context.get(userContext);
204+
const profile = await getProfile(user);
205+
return { profile };
206+
}
207+
208+
export default function Profile() {
209+
let loaderData = useLoaderData();
210+
return (
211+
<div>
212+
<h1>Welcome {loaderData.profile.fullName}!</h1>
213+
<Profile profile={loaderData.profile} />
214+
</div>
215+
);
216+
}
217+
```
218+
219+
### 4. Add an `unstable_getContext()` function (optional)
220+
221+
If you wish to include a base context on all navigations/fetches, you can add an `unstable_getContext` function to your router. This will be called to populate a fresh context on every navigation/fetch.
222+
223+
```tsx
224+
let sessionContext = unstable_createContext();
225+
226+
const router = createBrowserRouter(routes, {
227+
future: {
228+
unstable_middleware: true,
229+
},
230+
unstable_getContext() {
231+
let context = new unstable_RouterContextProvider();
232+
context.set(sessionContext, getSession());
233+
return context;
234+
},
235+
});
236+
```
237+
129238
## Core Concepts
130239

131240
### Server vs Client Middleware
132241

133-
**Server middleware** (`unstable_middleware`) runs on the server for:
242+
Server middleware runs on the server in Framework mode for HTML Document requests and `.data` requests for subsequent navigations and fetcher calls.
134243

135-
- HTML Document requests
136-
- `.data` requests for subsequent navigations and fetcher calls
244+
Because server middleware runs on the server in response to an HTTP `Request`, it returns an HTTP `Response` back up the middleware chain via the `next` function:
245+
246+
```ts
247+
async function serverMiddleware({ request }, next) {
248+
console.log(request.method, request.url);
249+
let response = await next();
250+
console.log(response.status, request.method, request.url);
251+
return response;
252+
}
253+
254+
export const unstable_middleware = [serverMiddleware];
255+
```
137256

138-
**Client middleware** (`unstable_clientMiddleware`) runs in the browser for:
257+
**Client middleware** (`unstable_clientMiddleware`) runs in the browser in framework and data mode for:
139258

140259
- Client-side navigations and fetcher calls
141260

261+
Client middleware is different because there's no HTTP Request, so it doesn't bubble up anything via the `next` function:
262+
263+
```ts
264+
async function clientMiddleware({ request }, next) {
265+
console.log(request.method, request.url);
266+
await next(); // 👈 No return value
267+
console.log(response.status, request.method, request.url);
268+
// 👈 No need to return anything here
269+
}
270+
271+
// Framework mode
272+
export const unstable_clientMiddleware = [clientMiddleware];
273+
274+
// Data mode
275+
const route = {
276+
path: "/",
277+
unstable_middleware: [clientMiddleware],
278+
loader: rootLoader,
279+
Component: Root,
280+
};
281+
```
282+
283+
### When Middleware Runs
284+
285+
It is very important to understand _when_ your middlewares will run to make sure your application is behaving as you intend.
286+
287+
#### Server Middleware
288+
289+
In 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_ request and only runs when you _need_ to hit the server.
290+
291+
This raises the question of what is a "handler" in React Router? Is it the route? Or the loader? We think "it depends":
292+
293+
- On document requests (`GET /route`), the handler is the route - because the response encompasses both the loader and the route component
294+
- On data requests (`GET /route.data`) for client-side navigations, the handler is the `loader`/`action`, because that's all that is included in the response
295+
296+
Therefore:
297+
298+
- Document requests run server middleware whether loaders exist or not because we're still in a "handler" to render the UI
299+
- Client-side navigations will only run server middleware if a `.data` request is made to the server for a `loader`/`action`
300+
301+
This 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.
302+
303+
```tsx filename=app/root.tsx
304+
// This middleware won't run on client-side navigations without a `.data` request
305+
function loggingMiddleware({ request }, next) {
306+
console.log(`Request: ${request.method} ${request.url}`);
307+
let response = await next();
308+
console.log(
309+
`Response: ${response.status} ${request.method} ${request.url}`,
310+
);
311+
return response;
312+
}
313+
314+
export const unstable_middleware = [loggingMiddleware];
315+
```
316+
317+
However, there may be cases where you _want_ to run certain 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 then when they submit to the `action`. If your middleware meets this 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.
318+
319+
```tsx filename=app/_auth.tsx
320+
function authMiddleware({ request }, next) {
321+
if (!isLoggedIn(request)) {
322+
throw redirect("/login");
323+
}
324+
}
325+
326+
export const unstable_middleware = [authMiddleware];
327+
328+
// By adding a loader, we force the authMiddleware to run on every client-side
329+
// navigation involving this route.
330+
export function loader() {
331+
return null;
332+
}
333+
```
334+
335+
#### Client Middleware
336+
337+
Client 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 or not there are loaders to run.
338+
142339
### Context API
143340

144341
The new context system provides type safety and prevents naming conflicts:
@@ -428,31 +625,5 @@ export async function loader({
428625
}
429626
```
430627

431-
## Client-Side Middleware
432-
433-
Client middleware works similar to server-side middleware but doesn't return responses because it's not running ion response to an HTTP `Request`:
434-
435-
```tsx filename=app/routes/dashboard.tsx
436-
import { userContext } from "~/context";
437-
438-
export const unstable_clientMiddleware = [
439-
({ context }) => {
440-
// Set up client-side user data
441-
const user = getLocalUser();
442-
context.set(userContext, user);
443-
},
444-
445-
async ({ context }, next) => {
446-
console.log("Starting client navigation");
447-
await next(); // 👈 No return value
448-
console.log("Client navigation complete");
449-
},
450-
];
451-
452-
export async function clientLoader({
453-
context,
454-
}: Route.ClientLoaderArgs) {
455-
const user = context.get(userContext);
456-
return { user };
457-
}
458-
```
628+
[server-client]: #server-vs-client-middleware
629+
[getloadcontext]: #changes-to-getloadcontextapploadcontext

docs/start/data/route-object.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,53 @@ function MyRouteComponent() {
5454
}
5555
```
5656

57+
## `unstable_middleware`
58+
59+
Route middleware runs sequentially before and after navigations. This gives you a singular place to do things like logging and authentication. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation.
60+
61+
```tsx
62+
createBrowserRouter([
63+
{
64+
path: "/",
65+
unstable_middleware: [loggingMiddleware],
66+
loader: rootLoader,
67+
Component: Root,
68+
children: [{
69+
path: 'auth',
70+
unstable_middleware: [authMiddleware],
71+
loader: authLoader,
72+
Component: Auth,
73+
children: [...]
74+
}]
75+
},
76+
]);
77+
78+
async function loggingMiddleware({ request }, next) {
79+
let url = new URL(request.url);
80+
console.log(`Starting navigation: ${url.pathname}${url.search}`);
81+
const start = performance.now();
82+
await next();
83+
const duration = performance.now() - start;
84+
console.log(`Navigation completed in ${duration}ms`);
85+
}
86+
87+
const userContext = unstable_createContext<User>();
88+
89+
async function authMiddleware ({ context }) {
90+
const userId = getUserId();
91+
92+
if (!userId) {
93+
throw redirect("/login");
94+
}
95+
96+
context.set(userContext, await getUserById(userId));
97+
};
98+
```
99+
100+
See also:
101+
102+
- [Middleware][middleware]
103+
57104
## `loader`
58105

59106
Route loaders provide data to route components before they are rendered.
@@ -198,3 +245,4 @@ createBrowserRouter([
198245
Next: [Data Loading](./data-loading)
199246

200247
[loader-params]: https://api.reactrouter.com/v7/interfaces/react_router.LoaderFunctionArgs
248+
[middleware]: ../../how-to/middleware

0 commit comments

Comments
 (0)