Skip to content

Commit 5f6b341

Browse files
authored
Add support for client context and middleware (unstable) (#12941)
1 parent 4da1dee commit 5f6b341

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+7944
-660
lines changed

.changeset/middleware.md

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Support middleware on routes (unstable)
6+
7+
Middleware is implemented behind a `future.unstable_middleware` flag. To enable, you must enable the flag and the types in your `react-router-config.ts` file:
8+
9+
```ts
10+
import type { Config } from "@react-router/dev/config";
11+
import type { Future } from "react-router";
12+
13+
declare module "react-router" {
14+
interface Future {
15+
unstable_middleware: true; // 👈 Enable middleware types
16+
}
17+
}
18+
19+
export default {
20+
future: {
21+
unstable_middleware: true, // 👈 Enable middleware
22+
},
23+
} satisfies Config;
24+
```
25+
26+
⚠️ Middleware is unstable and should not be adopted in production. There is at least one known de-optimization in route module loading for `clientMiddleware` that we will be addressing this before a stable release.
27+
28+
⚠️ Enabling middleware contains a breaking change to the `context` parameter passed to your `loader`/`action` functions - see below for more information.
29+
30+
Once enabled, routes can define an array of middleware functions that will run sequentially before route handlers run. These functions accept the same parameters as `loader`/`action` plus an additional `next` parameter to run the remaining data pipeline. This allows middlewares to perform logic before and after handlers execute.
31+
32+
```tsx
33+
// Framework mode
34+
export const unstable_middleware = [serverLogger, serverAuth]; // server
35+
export const unstable_clientMiddleware = [clientLogger]; // client
36+
37+
// Library mode
38+
const routes = [
39+
{
40+
path: "/",
41+
// Middlewares are client-side for library mode SPA's
42+
unstable_middleware: [clientLogger, clientAuth],
43+
loader: rootLoader,
44+
Component: Root,
45+
},
46+
];
47+
```
48+
49+
Here's a simple example of a client-side logging middleware that can be placed on the root route:
50+
51+
```tsx
52+
const clientLogger: Route.unstable_ClientMiddlewareFunction = async (
53+
{ request },
54+
next
55+
) => {
56+
let start = performance.now();
57+
58+
// Run the remaining middlewares and all route loaders
59+
await next();
60+
61+
let duration = performance.now() - start;
62+
console.log(`Navigated to ${request.url} (${duration}ms)`);
63+
};
64+
```
65+
66+
Note that in the above example, the `next`/`middleware` functions don't return anything. This is by design as on the client there is no "response" to send over the network like there would be for middlewares running on the server. The data is all handled behind the scenes by the stateful `router`.
67+
68+
For a server-side middleware, the `next` function will return the HTTP `Response` that React Router will be sending across the wire, thus giving you a chance to make changes as needed. You may throw a new response to short circuit and respond immediately, or you may return a new or altered response to override the default returned by `next()`.
69+
70+
```tsx
71+
const serverLogger: Route.unstable_MiddlewareFunction = async (
72+
{ request, params, context },
73+
next
74+
) => {
75+
let start = performance.now();
76+
77+
// 👇 Grab the response here
78+
let res = await next();
79+
80+
let duration = performance.now() - start;
81+
console.log(`Navigated to ${request.url} (${duration}ms)`);
82+
83+
// 👇 And return it here (optional if you don't modify the response)
84+
return res;
85+
};
86+
```
87+
88+
You can throw a `redirect` from a middleware to short circuit any remaining processing:
89+
90+
```tsx
91+
import { sessionContext } from "../context";
92+
const serverAuth: Route.unstable_MiddlewareFunction = (
93+
{ request, params, context },
94+
next
95+
) => {
96+
let session = context.get(sessionContext);
97+
let user = session.get("user");
98+
if (!user) {
99+
session.set("returnTo", request.url);
100+
throw redirect("/login", 302);
101+
}
102+
};
103+
```
104+
105+
_Note that in cases like this where you don't need to do any post-processing you don't need to call the `next` function or return a `Response`._
106+
107+
Here's another example of using a server middleware to detect 404s and check the CMS for a redirect:
108+
109+
```tsx
110+
const redirects: Route.unstable_MiddlewareFunction = async ({
111+
request,
112+
next,
113+
}) => {
114+
// attempt to handle the request
115+
let res = await next();
116+
117+
// if it's a 404, check the CMS for a redirect, do it last
118+
// because it's expensive
119+
if (res.status === 404) {
120+
let cmsRedirect = await checkCMSRedirects(request.url);
121+
if (cmsRedirect) {
122+
throw redirect(cmsRedirect, 302);
123+
}
124+
}
125+
126+
return res;
127+
};
128+
```
129+
130+
**`context` parameter**
131+
132+
When middleware is enabled, your application will use a different type of `context` parameter in your loaders and actions to provide better type safety. Instead of `AppLoadContext`, `context` will now be an instance of `ContextProvider` that you can use with type-safe contexts (similar to `React.createContext`):
133+
134+
```ts
135+
import { unstable_createContext } from "react-router";
136+
import { Route } from "./+types/root";
137+
import type { Session } from "./sessions.server";
138+
import { getSession } from "./sessions.server";
139+
140+
let sessionContext = unstable_createContext<Session>();
141+
142+
const sessionMiddleware: Route.unstable_MiddlewareFunction = ({
143+
context,
144+
request,
145+
}) => {
146+
let session = await getSession(request);
147+
context.set(sessionContext, session);
148+
// ^ must be of type Session
149+
};
150+
151+
// ... then in some downstream middleware
152+
const loggerMiddleware: Route.unstable_MiddlewareFunction = ({
153+
context,
154+
request,
155+
}) => {
156+
let session = context.get(sessionContext);
157+
// ^ typeof Session
158+
console.log(session.get("userId"), request.method, request.url);
159+
};
160+
161+
// ... or some downstream loader
162+
export function loader({ context }: Route.LoaderArgs) {
163+
let session = context.get(sessionContext);
164+
let profile = await getProfile(session.get("userId"));
165+
return { profile };
166+
}
167+
```
168+
169+
If you are using a custom server with a `getLoadContext` function, the return value for initial context values passed from the server adapter layer is no longer an object and should now return an `unstable_InitialContext` (`Map<RouterContext, unknown>`):
170+
171+
```ts
172+
let adapterContext = unstable_createContext<MyAdapterContext>();
173+
174+
function getLoadContext(req, res): unstable_InitialContext {
175+
let map = new Map();
176+
map.set(adapterContext, getAdapterContext(req));
177+
return map;
178+
}
179+
```

.changeset/spa-context.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Add `context` support to client side data routers (unstable)
6+
7+
Your application `loader` and `action` functions on the client will now receive a `context` parameter. This is an instance of `unstable_RouterContextProvider` that you use with type-safe contexts (similar to `React.createContext`) and is most useful with the corresponding `middleware`/`clientMiddleware` API's:
8+
9+
```ts
10+
import { unstable_createContext } from "react-router";
11+
12+
type User = {
13+
/*...*/
14+
};
15+
16+
let userContext = unstable_createContext<User>();
17+
18+
function sessionMiddleware({ context }) {
19+
let user = await getUser();
20+
context.set(userContext, user);
21+
}
22+
23+
// ... then in some downstream loader
24+
function loader({ context }) {
25+
let user = context.get(userContext);
26+
let profile = await getProfile(user.id);
27+
return { profile };
28+
}
29+
```
30+
31+
Similar to server-side requests, a fresh `context` will be created per navigation (or `fetcher` call). If you have initial data you'd like to populate in the context for every request, you can provide an `unstable_getContext` function at the root of your app:
32+
33+
- Library mode - `createBrowserRouter(routes, { unstable_getContext })`
34+
- Framework mode - `<HydratedRouter unstable_getContext>`
35+
36+
This function should return an value of type `unstable_InitialContext` which is a `Map<unstable_RouterContext, unknown>` of context's and initial values:
37+
38+
```ts
39+
const loggerContext = unstable_createContext<(...args: unknown[]) => void>();
40+
41+
function logger(...args: unknown[]) {
42+
console.log(new Date.toISOString(), ...args);
43+
}
44+
45+
function unstable_getContext() {
46+
let map = new Map();
47+
map.set(loggerContext, logger);
48+
return map;
49+
}
50+
```

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ packages/react-router-dom/server.d.ts
1010
packages/react-router-dom/server.js
1111
packages/react-router-dom/server.mjs
1212
tutorial/dist/
13+
public/

0 commit comments

Comments
 (0)