Skip to content

Commit 03c68bd

Browse files
committed
Merge branch 'release-next'
2 parents 71be3d6 + d3bed18 commit 03c68bd

File tree

162 files changed

+11379
-1802
lines changed

Some content is hidden

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

162 files changed

+11379
-1802
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
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/

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ node_modules/
3030
.eslintcache
3131
.tmp
3232
tsup.config.bundled_*.mjs
33+
build.utils.d.ts
3334
/.env
3435
/NOTES.md
3536

CHANGELOG.md

Lines changed: 412 additions & 103 deletions
Large diffs are not rendered by default.

contributors.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- akamfoad
1717
- alany411
1818
- alberto
19+
- Aleuck
1920
- alexandernanberg
2021
- alexanderson1993
2122
- alexlbr
@@ -144,6 +145,7 @@
144145
- JaffParker
145146
- jakkku
146147
- JakubDrozd
148+
- jamesopstad
147149
- jamesrwilliams
148150
- janpaepke
149151
- jasikpark
@@ -268,6 +270,7 @@
268270
- robbtraister
269271
- RobHannay
270272
- robinvdvleuten
273+
- rossipedia
271274
- rtmann
272275
- rtzll
273276
- rubeonline
@@ -329,6 +332,7 @@
329332
- tosinamuda
330333
- triangularcube
331334
- trungpv1601
335+
- TrySound
332336
- ttys026
333337
- Tumas2
334338
- turansky

decisions/0014-context-middleware.md

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# Middleware + Context
2+
3+
Date: 2025-01-22
4+
5+
Status: accepted
6+
7+
## Context
8+
9+
_Lol "context", get it 😉_
10+
11+
The [Middleware RFC][rfc] is the _most-upvoted_ RFC/Proposal in the React Router repo. We actually tried to build and ship it quite some time ago but realized that without single fetch it didn't make much sense in an SSR world for 2 reasons:
12+
13+
- With the individual HTTP requests per loader, middleware wouldn't actually reduce the # of queries to your DB/API's - it would just be a code convenience with no functional impact
14+
- Individual HTTP requests meant a lack of a shared request scope across routes
15+
16+
We've done a lot of work since then to get us to a place where we could ship a middleware API we were happy with:
17+
18+
- Shipped [Single Fetch][single-fetch]
19+
- Shipped [`dataStrategy`][data-strategy] for DIY middleware in React Router SPAs
20+
- Iterated on middleware/context APIs in the [Remix the Web][remix-the-web] project
21+
- Developed a non-invasive type-safe + composable [context][async-provider] API
22+
23+
## Decision
24+
25+
### Leverage a new type-safe `context` API
26+
27+
We originally considered leaning on our existing `context` (`type AppLoadContext`) value we pass to server-side `loader` and `action` functions as the `context` for middleware functions. Using this would make for an easier adoption of middleware for apps that use `AppLoadContext` today. However, there were a few downsides to that approach.
28+
29+
First, the type story is lacking because it's just a global interface you augment via declaration merging so it's not true type safety and is more of a "trust me on this" scenario. We've always known it wasn't a great typed API and have always assumed we'd enhance it at some point via a breaking change behind a future flag. The introduction of middleware should result in much _more_ usage of `context` than exists today since it'll open up to user of `react-router-serve` as well. For this reason it made more sense to ship the breaking change flag now for the smaller surface area of `context`-enabled apps users, instead of later for a much larger surface area of apps.
30+
31+
Second, in order to implement client-side middleware, we need to introduce a new `context` concept on the client - and we would like that to be the same API as we have on the server. So, if we chose to stick with `AppLoadContext`, we'd then have to implement a brand new `ClientAppLoadContext` which would suffer the same type issues out of the gate. It felt lazy to ship a known-subpar-API to the client. Furthermore, even if we did ship it - we'd _still_ want to enhance it later - so we'd be shipping a mediocre client `context` API _knowing_ that we would be breaking shortly after with a better typed API.
32+
33+
That is why we decided to rip the band-aid off and include the breaking `context` change with the initial release of middleware. When the flag is enabled, we'll be replacing `AppLoadContext` with a new type-safe `context` API that is similar in usage to the `React.createContext` API:
34+
35+
```ts
36+
let userContext = unstable_createContext<User>();
37+
38+
const userMiddleware: Route.unstable_MiddlewareFunction = async ({
39+
context,
40+
request,
41+
}) => {
42+
context.set(userContext, await getUser(request));
43+
};
44+
45+
export const middleware = [userMiddleware];
46+
47+
// In some other route
48+
export async function loader({ context }: Route.LoaderArgs) {
49+
let user = context.get(userContext);
50+
let posts = await getPosts(user);
51+
return { posts };
52+
}
53+
```
54+
55+
If you have an app already using `AppLoadContext`, you don't need to split that out, and can instead stick that object into it's own context value and maintain the same shape:
56+
57+
```diff
58+
+ let appContext = unstable_createContext<AppLoadContext>()
59+
60+
function getLoadContext(req, res) {
61+
let appLoadContext = { /* your existing object */ };
62+
63+
- return appLoadContext
64+
+ return new Map([[appContext, appLoadContext]]);
65+
}
66+
67+
function loader({ context }) {
68+
- context.foo.something();
69+
+ // Hopefully this can be done via find/replace or a codemod
70+
+ context.get(appContext).foo.something()
71+
// ...
72+
}
73+
```
74+
75+
#### Client Side Context
76+
77+
In order to support the same API on the client, we will also add support for a client-side `context` of the same type (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>`:
78+
79+
```ts
80+
let loggerContext = unstable_createContext<(...args: unknown[]) => void>();
81+
82+
function getContext() {
83+
return new Map([[loggerContext, (...args) => console.log(...args)]])
84+
}
85+
86+
// library mode
87+
let router = createBrowserRouter(routes, { unstable_getContext: getContext })
88+
89+
// framework mode
90+
return <HydratedRouter unstable_getContext={getContext}>
91+
```
92+
93+
`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.
94+
95+
### API
96+
97+
We wanted our middleware API to meet a handful of criteria:
98+
99+
- Allow users to perform logic sequentially top-down before handlers are called
100+
- Allow users to modify the outgoing response bottom-up after handlers are called
101+
- Allow multiple middlewares per route
102+
103+
The middleware API we landed on to ship looks as follows:
104+
105+
```ts
106+
const myMiddleware: Route.unstable_MiddlewareFunction = async (
107+
{ request, context },
108+
next
109+
) => {
110+
// Do stuff before the handlers are called
111+
context.user = await getUser(request);
112+
// Call handlers and generate the Response
113+
let res = await next();
114+
// Amend the response if needed
115+
res.headers.set("X-Whatever", "stuff");
116+
// Propagate the response up the middleware chain
117+
return res;
118+
};
119+
120+
// Export an array of middlewares per-route which will run left-to-right on
121+
// the server
122+
export const middleware = [myMiddleware];
123+
124+
// You can also export an array of client middlewares that run before/after
125+
// `clientLoader`/`clientAction`
126+
const myClientMiddleware: Route.unstable_ClientMiddlewareFunction = (
127+
{ context },
128+
next
129+
) => {
130+
//...
131+
};
132+
133+
export const clientMiddleware = [myClientSideMiddleware];
134+
```
135+
136+
If you only want to perform logic _before_ the request, you can skip calling the `next` function and it'll be called and the response propagated upwards for you automatically:
137+
138+
```ts
139+
const myMiddleware: Route.unstable_MiddlewareFunction = async ({
140+
request,
141+
context,
142+
}) => {
143+
context.user = await getUser(request);
144+
// Look ma, no next!
145+
};
146+
```
147+
148+
The only nuance between server and client middleware is that on the server, we want to propagate a `Response` back up the middleware chain, so `next` must call the handlers _and_ generate the final response. In document requests, this will be the rendered HTML document, and in data requests this will be the `turbo-stream` `Response`.
149+
150+
Client-side navigations don't really have this type of singular `Response` - they're just updating a stateful router and triggering a React re-render. Therefore, there is no response to bubble back up and the next function will run handlers but won't return anything so there's nothing to propagate back up the middleware chain.
151+
152+
### Client-side Implementation
153+
154+
For client side middleware, up until now we've been recommending that if folks want middleware they can add it themselves using `dataStrategy`. Therefore, we can leverage that API and add our middleware implementation inside our default `dataStrategy`. This has the primary advantage of being very simple to implement, but it also means that if folks decide to take control of their own `dataStrategy`, then they take control of the _entire_ data flow. It would have been confusing if a user provided a custom `dataStrategy` in which they wanted to do their own middleware approach - and the router was still running it's own middleware logic before handing off to `dataStrategy`.
155+
156+
If users _want_ to take control over `loader`/`action` execution but still want to use our middleware flows, we should provide an API for them to do so. The current thought here is to pass them a utility into `dataStrategy` they can leverage:
157+
158+
```ts
159+
async function dataStrategy({ request, matches, defaultMiddleware }) {
160+
let results = await defaultMiddleware(() => {
161+
// custom loader/action execution logic here
162+
});
163+
return results;
164+
}
165+
```
166+
167+
One consequence of implementing middleware as part of `dataStrategy` is that on client-side submission requests it will run once for the action and again for the loaders. We went back and forth on this a bit and decided this was the right approach because it mimics the current behavior of SPA navigations in a full-stack React Router app since actions and revalidations are separate HTTP requests and thus run the middleware chains independently. We don't expect this to be an issue except in expensive middlewares - and in those cases the context will be shared between the action/loader chains and the second execution can be skipped if necessary:
168+
169+
```ts
170+
const expensiveMiddleware: Route.unstable_ClientMiddleware = async function ({
171+
request,
172+
context,
173+
}) {
174+
// Guard this such that we use the existing value if it exists from the action pass
175+
context.something = context.something ?? (await getExpensiveValue());
176+
};
177+
```
178+
179+
**Note:** This will make more sense after reading the next section, but it's worth noting that client middlewares _have_ to be run as part of `dataStrategy` to avoid running middlewares for loaders which have opted out of revalidation. The `shouldRevalidate` function decodes which loaders to run and does so using the `actionResult` as an input. so it's impossible to decide which loaders will be _prior_ to running the action. So we need to run middleware once for the action and again for the chosen loaders.
180+
181+
### Server-Side Implementation
182+
183+
Server-side middleware is a bit trickier because it needs to propagate a Response back upwards. This means that it _can't_ be done via `dataStrategy` because on document POST requests we need to know the results of _both_ the action and the loaders so we can render the HTML response. And we need to render the HTML response a single time in `next`, which means middleware can only be run once _per request_ - not once for actions and once for loaders.
184+
185+
This is an important concept to grasp because it points out a nuance between document and data requests. GET navigations will behave the same because there is a single request/response for both document and data GET navigations. POST navigations are different though:
186+
187+
- A document POST navigation (JS unavailable) is a single request/response to call action+loaders and generate a single HTML response.
188+
- A data POST navigation (JS available) is 2 separate request/response's - one to call the action and a second revalidation call for the loaders.
189+
190+
This means that there may be a slight difference in behavior of your middleware when it comes to loaders if you begin doing request-specific logic:
191+
192+
```ts
193+
function weirdMiddleware({ request }) {
194+
if (request.method === "POST") {
195+
// ✅ Runs before the action/loaders on document submissions
196+
// ✅ Runs before the action on data submissions
197+
// ❌ Does not runs before the loaders on data submission revalidations
198+
}
199+
}
200+
```
201+
202+
Our suggestion is mostly to avoid doing request-specific logic in middlewares, and if you need to do so, be aware of the behavior differences between document and data requests.
203+
204+
### Scenarios
205+
206+
The below outlines a few sample scenarios to give you an idea of the flow through middleware chains.
207+
208+
The simplest scenario is a document `GET /a/b` request:
209+
210+
- Start a `middleware`
211+
- Start b `middleware`
212+
- Run a/b `loaders` in parallel
213+
- Render HTML `Response` to bubble back up via `next()`
214+
- Finish b `middleware`
215+
- Finish a `middleware`
216+
217+
If we introduce `clientMiddleware` but no `clientLoader` and client-side navigate to `/a/b`:
218+
219+
- Start a `clientMiddleware`
220+
- Start b `clientMiddleware`
221+
- `GET /a/b.data`
222+
- Start a `middleware`
223+
- Start b `middleware`
224+
- Run a/b `loaders` in parallel
225+
- Render HTML `Response` to bubble back up via `next()`
226+
- Finish b `middleware`
227+
- Finish a `middleware`
228+
- Respond to client
229+
- Finish b `clientMiddleware`
230+
- Finish a `clientMiddleware`
231+
232+
If we have `clientLoaders` and they don't call server `loaders` (SPA Mode):
233+
234+
- Start a `clientMiddleware`
235+
- Start b `clientMiddleware`
236+
- Run a/b `clientLoaders` in parallel
237+
- _No Response to render here so we can either bubble up `undefined` or potentially a `Location`_
238+
- `Location` feels maybe a bit weird and introduces another way to redirect instead of `throw redirect`...
239+
- Finish b `clientMiddleware`
240+
- Finish a `clientMiddleware`
241+
242+
If `clientLoaders` do call `serverLoaders` it gets trickier since they make individual server requests:
243+
244+
- Start a `clientMiddleware`
245+
- Start b `clientMiddleware`
246+
- Run a/b `clientLoaders` in parallel
247+
- `a` `clientLoader` calls GET `/a/b.data?route=a`
248+
- Start a `middleware`
249+
- Run a loader
250+
- Render turbo-stream `Response` to bubble back up via `next()`
251+
- Finish a `middleware`
252+
- `b` `clientLoader` calls GET `/a/b.data?route=b`
253+
- Start a `middleware`
254+
- Start b `middleware`
255+
- Run b loader
256+
- Render turbo-stream `Response` to bubble back up via `next()`
257+
- Finish b `middleware`
258+
- Finish a `middleware`
259+
- Finish b `clientMiddleware`
260+
- Finish a `clientMiddleware`
261+
262+
### Other Thoughts
263+
264+
- Middleware is data-focused, not an event system
265+
- you should not be relying on middleware to track how many users hit a certain page etc
266+
- middleware may run once for actions and once for loaders
267+
- middleware will run independently for navigational loaders and fetcher loaders
268+
- middleware may run many times for revalidations
269+
- middleware may not run for revalidation opt outs
270+
- Middleware allows you to run logic specific to a branch of the tree before/after data fns
271+
- logging
272+
- auth/redirecting
273+
- 404 handling
274+
275+
[rfc]: https://github.com/remix-run/react-router/discussions/9564
276+
[client-context]: https://github.com/remix-run/react-router/discussions/9856
277+
[single-fetch]: https://remix.run/docs/en/main/guides/single-fetch
278+
[data-strategy]: https://reactrouter.com/v6/routers/create-browser-router#optsdatastrategy
279+
[remix-the-web]: https://github.com/mjackson/remix-the-web
280+
[async-provider]: https://github.com/ryanflorence/async-provider

docs/how-to/pre-rendering.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Pre-Rendering
66

77
Pre-Rendering allows you to speed up page loads for static content by rendering pages at build time instead of at runtime. Pre-rendering is enabled via the `prerender` config in `react-router.config.ts` and can be used in two ways based on the `ssr` config value:
88

9-
- Alongside a runtime SSR server ith `ssr:true` (the default value)
9+
- Alongside a runtime SSR server with `ssr:true` (the default value)
1010
- Deployed to a static file server with `ssr:false`
1111

1212
## Pre-rendering with `ssr:true`
@@ -86,7 +86,7 @@ During development, pre-rendering doesn't save the rendered results to the publi
8686

8787
## Pre-rendering with `ssr:false`
8888

89-
The above examples assume you are deploying a runtime server, but are pre-rendering some static pages in order to serve them faster and avoid hitting the server.
89+
The above examples assume you are deploying a runtime server but are pre-rendering some static pages to avoid hitting the server, resulting in faster loads.
9090

9191
To disable runtime SSR and configure pre-rendering to be served from a static file server, you can set the `ssr:false` config flag:
9292

0 commit comments

Comments
 (0)