Skip to content

Commit d499f66

Browse files
committed
Updates
1 parent 4f9d351 commit d499f66

File tree

11 files changed

+147
-79
lines changed

11 files changed

+147
-79
lines changed

.changeset/fresh-buttons-sit.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

.changeset/middleware.md

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,30 @@
22
"react-router": patch
33
---
44

5-
Support `middleware` on routes (unstable)
5+
Support middleware on routes (unstable)
66

7-
Routes can now define an array of middleware functions that will run sequentially before route handlers run. These functions accept the same arguments as `loader`/`action` plus an additional `next` function to run the remaining data pipeline. This allows middlewares to perform logic before and after handlers execute.
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+
⚠️ Enabling middleware contains a breaking change to the `context` parameter passed to your `loader`/`action` functions - see below for more information.
27+
28+
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.
829

930
```tsx
1031
// Framework mode
@@ -25,33 +46,23 @@ const routes = [
2546
Here's a simple example of a client-side logging middleware that can be placed on the root route:
2647

2748
```tsx
28-
async function clientLogger({
29-
request,
30-
params,
31-
context,
32-
next,
33-
}: Route.ClientMiddlewareArgs) {
49+
async function clientLogger({ request }, next) {
3450
let start = performance.now();
3551

3652
// Run the remaining middlewares and all route loaders
3753
await next();
3854

3955
let duration = performance.now() - start;
4056
console.log(`Navigated to ${request.url} (${duration}ms)`);
41-
}
57+
} satisfies Route.ClientMiddlewareFunction;
4258
```
4359

4460
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`.
4561

4662
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()`.
4763

4864
```tsx
49-
async function serverLogger({
50-
request,
51-
params,
52-
context,
53-
next,
54-
}: Route.MiddlewareArgs) {
65+
async function serverLogger({ request, params, context }, next) {
5566
let start = performance.now();
5667

5768
// 👇 Grab the response here
@@ -62,21 +73,21 @@ async function serverLogger({
6273

6374
// 👇 And return it here
6475
return res;
65-
}
76+
} satisfies Route.MiddlewareFunction;
6677
```
6778

6879
You can throw a `redirect` from a middleware to short circuit any remaining processing:
6980

7081
```tsx
71-
function serverAuth({ request, params, context, next }: Route.MiddlewareArgs) {
72-
let user = context.session.get("user");
82+
import { sessionContext } from "../context";
83+
function serverAuth({ request, params, context }, next) {
84+
let session = context.get(sessionContext);
85+
let user = session.get("user");
7386
if (!user) {
74-
context.session.set("returnTo", request.url);
87+
session.set("returnTo", request.url);
7588
throw redirect("/login", 302);
7689
}
77-
context.user = user;
78-
// No need to call next() if you don't need to do any post processing
79-
}
90+
} satisfies Route.MiddlewareFunction;
8091
```
8192

8293
_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`._
@@ -100,3 +111,35 @@ async function redirects({ request, next }: Route.MiddlewareArgs) {
100111
return res;
101112
}
102113
```
114+
115+
**`context` parameter**
116+
117+
When middleware is enabled, your application wil use a different type of `context` parameter in your loaders and actions to provide better type safety. `context` will now be an instance of `ContextProvider` that you use with type-safe contexts (similar to `React.createContext`):
118+
119+
```ts
120+
import { unstable_createContext } from "react-router";
121+
import { Route } from "./+types/root";
122+
import type { Session } from "./sessions.server";
123+
import { getSession } from "./sessions.server";
124+
125+
let sessionContext = unstable_createContext<Session>();
126+
127+
export function sessionMiddleware({ context, request }) {
128+
let session = await getSession(request);
129+
context.set(sessionContext, session);
130+
} satisfies Route.MiddlewareFunction;
131+
132+
// ... then in some downstream middleware
133+
export function loggerMiddleware({ context, request }) {
134+
let session = context.get(sessionContext);
135+
// ^ typeof Session
136+
console.log(session.get("userId"), request.method, request.url);
137+
} satisfies Route.MiddlewareFunction;
138+
139+
// ... or some downstream loader
140+
export function loader({ context }: Route.LoaderArgs) {
141+
let session = context.get(sessionContext);
142+
let profile = await getProfile(session.get('userId'));
143+
return { profile };
144+
}
145+
```

.changeset/spa-context.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,45 @@
44

55
Add `context` support to client side data routers (unstable)
66

7-
- Library mode - `createBrowserRouter(routes, { unstable_context })`
8-
- Framework mode - `<HydratedRouter unstable_context>`
7+
Your application `loader` and `action` functionsopn the client will now receive a `context` parameter. This is an instance of `ContextProvider` 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 oy your app:
32+
33+
- Library mode - `createBrowserRouter(routes, { unstable_getContext })`
34+
- Framework mode - `<HydratedRouter unstable_getContext>`
35+
36+
This function should return an instance of `unstable_InitialContext` which is a `Map` 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+
return new Map([[loggerContext, logger]]);
47+
}
48+
```

decisions/0014-context-middleware.md

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,64 +22,46 @@ We've done a lot of work since then to get us to a place where we could ship a m
2222

2323
## Decision
2424

25-
### Lean on existing `context` parameter for initial implementation
25+
### Leverage a new type-safe `context` API
2626

27-
During our experiments we realized that we could offload type-safe context to an external package. This would result in a simpler implementation within React Router and avoid the need to try to patch on type-safety to our existing `context` API which was designed as a quick escape hatch to cross the bridge from your server (i.e., `express` `req`/`res`) to the Remix handlers.
27+
We originally considered leaning on our existing `context` value we pass to server-side `loader` and `action` functions, and implementing a similar client-side equivalent for parity. However, the type story around `AppLoadContext` isn't great, so that would mean implementing a new API client side that we knew we weren't happy with from day one. And then likely replacing it with a better version fairly soon after.
2828

29-
Therefore, our recommendation for proper type-safe context for usage within middlewares and route handlers will be the [`@ryanflorence/async-provider`][async-provider] package.
30-
31-
If you don't want to adopt an extra package, or don't care about 100% type-safety and are happy with the module augmentation approach used by `AppLoadContext`, then you can use the existing `context` parameter passed to loaders and actions.
29+
Instead, when the flag is enabled, we'll be removing `AppLoadContext` in favor of a type-safe `context` API that is similar in usage to the `React.createContext` API:
3230

3331
```ts
34-
declare module "react-router" {
35-
interface AppLoadContext {
36-
user: User | null;
37-
}
38-
}
32+
let userContext = createContext<User>();
3933

40-
// root.tsx
41-
function userMiddleware({ request, context }: Route.MiddlewareArgs) {
42-
context.user = getUser(request);
43-
}
34+
async function userMiddleware({ context, request }) {
35+
context.set(userContext, await getUser(request));
36+
} satisfies Route.MiddlewareFunction;
4437

4538
export const middleware = [userMiddleware];
46-
n;
4739

4840
// In some other route
4941
export async function loader({ context }: Route.LoaderArgs) {
50-
let posts = await getPostsForUser(context.user);
42+
let user = context.get(userContext);
43+
let posts = await getPosts(user);
5144
return { posts };
5245
}
5346
```
5447

5548
#### Client Side Context
5649

57-
In order to support the same API on the client, we will need to add support for a client-side `context` value which is already a [long requested feature][client-context]. We can do so just like the server and let users use module augmentation to define their context shape. This will default to an empty object like the server, and can be passed to `createBrowserRouter` in library mode or `<HydratedRouter>` in framework mode to provide up-front singleton values.
50+
In order to support the same API on the client, we will also add support for a client-side `context` value which is already a [long requested feature][client-context]. If you need to provide initial values (similar to `getLoadContext` on the server), you can do so with a new `getContext` method which returns a `Map<RouterContext, unknown>`:
5851

5952
```ts
60-
declare module "react-router" {
61-
interface RouterContext {
62-
logger(...args: unknown[]): void
63-
}
53+
function getContext() {
54+
return new Map([[loggerContext, (...args) => console.log(...args)]])
6455
}
6556

66-
// Singleton fields that don't change and are always available
67-
let globalContext: RouterContext = { logger: getLogger() };
68-
6957
// library mode
70-
let router = createBrowserRouter(routes, { context: globalContext });
58+
let router = createBrowserRouter(routes, { getContext })
7159

7260
// framework mode
73-
return <HydratedRouter context={globalContext}>
74-
```
75-
76-
`context` on the server has the advantage of auto-cleanup since it's scoped to a request and thus automatically cleaned up after the request completes. In order to mimic this behavior on the client, we'll create a new object per navigation/fetch and spread in the properties from the global singleton context. Therefore, the context object you receive in your handlers will be:
77-
78-
```ts
79-
let scopedContext = { ...globalContext };
61+
return <HydratedRouter getContext={getContext}>
8062
```
8163

82-
This way, singleton values will always be there, but any new fields added to that object in middleware will only exist for that specific navigation or fetcher call and it will disappear once the request is complete.
64+
`context` on the server has the advantage of auto-cleanup since it's scoped to a request and thus automatically cleaned up after the request completes. In order to mimic this behavior on the client, we'll create a new object per navigation/fetch.
8365

8466
### API
8567

packages/react-router/__tests__/router/utils/data-router-setup.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type {
99
import type {
1010
AgnosticDataRouteObject,
1111
AgnosticRouteMatch,
12-
unstable_RouterContext,
1312
} from "../../../lib/router/utils";
1413
import { createRouter, IDLE_FETCHER } from "../../../lib/router/router";
1514
import {
@@ -145,7 +144,6 @@ type SetupOpts = {
145144
initialIndex?: number;
146145
hydrationData?: HydrationState;
147146
dataStrategy?: DataStrategyFunction;
148-
context?: unstable_RouterContext;
149147
};
150148

151149
// We use a slightly modified version of createDeferred here that includes the
@@ -201,7 +199,6 @@ export function setup({
201199
initialIndex,
202200
hydrationData,
203201
dataStrategy,
204-
context,
205202
}: SetupOpts) {
206203
let guid = 0;
207204
// Global "active" helpers, keyed by navType:guid:loaderOrAction:routeId.
@@ -339,7 +336,6 @@ export function setup({
339336
jest.spyOn(history, "replace");
340337
currentRouter = createRouter({
341338
basename,
342-
context,
343339
history,
344340
routes: enhanceRoutes(routes),
345341
hydrationData,

packages/react-router/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ export type {
4848
ShouldRevalidateFunctionArgs,
4949
UIMatch,
5050
} from "./lib/router/utils";
51-
export { unstable_createContext } from "./lib/router/utils";
51+
export {
52+
unstable_createContext,
53+
unstable_RouterContextProvider,
54+
} from "./lib/router/utils";
5255

5356
export {
5457
Action as NavigationType,

packages/react-router/lib/components.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import type {
2020
Router as DataRouter,
2121
RouterState,
2222
RouterSubscriber,
23+
Router,
24+
RouterInit,
2325
} from "./router/router";
2426
import { createRouter } from "./router/router";
2527
import type {
@@ -137,9 +139,9 @@ export interface MemoryRouterOpts {
137139
*/
138140
basename?: string;
139141
/**
140-
* Router context singleton that will be passed to loader/action functions.
142+
* Function to provide the initial context values for all client side navigations/fetches
141143
*/
142-
unstable_getContext?: () => unstable_InitialContext;
144+
unstable_getContext?: RouterInit["unstable_getContext"];
143145
/**
144146
* Future flags to enable for the router.
145147
*/

packages/react-router/lib/dom-export/hydrated-router.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
DataRouter,
77
HydrationState,
88
unstable_InitialContext,
9+
RouterInit,
910
} from "react-router";
1011
import {
1112
UNSAFE_invariant as invariant,
@@ -64,7 +65,7 @@ function initSsrInfo(): void {
6465
function createHydratedRouter({
6566
unstable_getContext,
6667
}: {
67-
unstable_getContext?: () => unstable_InitialContext;
68+
unstable_getContext?: RouterInit["unstable_getContext"];
6869
}): DataRouter {
6970
initSsrInfo();
7071

@@ -221,7 +222,7 @@ interface HydratedRouterProps {
221222
* Context object to passed through to `createBrowserRouter` and made available
222223
* to `clientLoader`/`clientActon` functions
223224
*/
224-
unstable_getContext?: () => unstable_InitialContext;
225+
unstable_getContext?: RouterInit["unstable_getContext"];
225226
}
226227

227228
/**

packages/react-router/lib/dom/lib.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ import type {
2323
HydrationState,
2424
RelativeRoutingType,
2525
Router as DataRouter,
26+
RouterInit,
2627
} from "../router/router";
2728
import { IDLE_FETCHER, createRouter } from "../router/router";
2829
import type {
2930
DataStrategyFunction,
3031
FormEncType,
3132
HTMLFormMethod,
3233
UIMatch,
33-
unstable_InitialContext,
3434
} from "../router/utils";
3535
import {
3636
ErrorResponseImpl,
@@ -135,9 +135,9 @@ export interface DOMRouterOpts {
135135
*/
136136
basename?: string;
137137
/**
138-
* Router context singleton that will be passed to loader/action functions.
138+
* Function to provide the initial context values for all client side navigations/fetches
139139
*/
140-
unstable_getContext?: () => unstable_InitialContext;
140+
unstable_getContext?: RouterInit["unstable_getContext"];
141141
/**
142142
* Future flags to enable for the router.
143143
*/

0 commit comments

Comments
 (0)