Skip to content

Commit bd341ff

Browse files
authored
feat(react-router): Update client middleware to propagate data strategy results up the chain (#14151)
1 parent 5ebc1cb commit bd341ff

File tree

5 files changed

+208
-253
lines changed

5 files changed

+208
-253
lines changed

.changeset/violet-dots-collect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
[UNSTABLE] Update client middleware so it returns the data strategy results allowing for more advanced post-processing middleware

docs/how-to/middleware.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,13 @@ export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
256256
[serverMiddleware];
257257
```
258258

259-
Client middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware is different because there's no HTTP [`Request`][request], so it doesn't bubble up anything via the `next` function:
259+
Client middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware differs from server middleware because there's no HTTP Request, so it doesn't have a `Response` to bubble up. In most cases, you can just ignore the return value from `next` and return nothing from your middleware on the client:
260260

261261
```ts
262262
async function clientMiddleware({ request }, next) {
263263
console.log(request.method, request.url);
264-
await next(); // 👈 No return value
264+
await next();
265265
console.log(response.status, request.method, request.url);
266-
// 👈 No need to return anything here
267266
}
268267

269268
// Framework mode
@@ -279,6 +278,34 @@ const route = {
279278
};
280279
```
281280

281+
There may be _some_ cases where you want to do some post-processing based on the result of the loaders/action. In lieu of a `Response`, client middleware bubbles up the value returned from the active [`dataStrategy`][datastrategy] (`Record<string, DataStrategyResult>` - keyed by route id). This allows you to take conditional action in your middleware based on the outcome of the executed `loader`/`action` functions.
282+
283+
Here's an example of the [CMS Redirect on 404][cms-redirect] use case implemented as a client side middleware:
284+
285+
```tsx
286+
async function cmsFallbackMiddleware({ request }, next) {
287+
const results = await next();
288+
289+
// Check if we got a 404 from any of our routes and if so, look for a
290+
// redirect in our CMS
291+
const found404 = Object.values(results).some(
292+
(r) =>
293+
isRouteErrorResponse(r.result) &&
294+
r.result.status === 404,
295+
);
296+
if (found404) {
297+
const cmsRedirect = await checkCMSRedirects(
298+
request.url,
299+
);
300+
if (cmsRedirect) {
301+
throw redirect(cmsRedirect, 302);
302+
}
303+
}
304+
}
305+
```
306+
307+
<docs-warning>In a server middleware, you shouldn't be messing with the `Response` body and should only be reading status/headers and setting headers. Similarly, this value should be considered read-only in client middleware because it represents the "body" or "data" for the resulting navigation which should be driven by loaders/actions - not middleware. This also means that in client middleware, there's usually no need to return the results even if you needed to capture it from `await next()`;</docs-warning>
308+
282309
### When Middleware Runs
283310

284311
It is very important to understand _when_ your middlewares will run to make sure your application is behaving as you intend.
@@ -606,7 +633,7 @@ export const loggingMiddleware = async (
606633
};
607634
```
608635

609-
### 404 to CMS Redirect
636+
### CMS Redirect on 404
610637

611638
```tsx filename=app/middleware/cms-fallback.ts
612639
export const cmsFallbackMiddleware = async (
@@ -703,6 +730,8 @@ export async function loader({
703730
[framework-action]: ../start/framework/route-module#action
704731
[framework-loader]: ../start/framework/route-module#loader
705732
[getloadcontext]: #changes-to-getloadcontextapploadcontext
733+
[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
734+
[cms-redirect]: #cms-redirect-on-404
706735
[createContext]: ../api/utils/createContext
707736
[RouterContextProvider]: ../api/utils/RouterContextProvider
708737
[getContext]: ../api/data-routers/createBrowserRouter#optsunstable_getContext
@@ -716,5 +745,3 @@ export async function loader({
716745
[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support
717746
[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage
718747
[ErrorBoundary]: ../start/framework/route-module#errorboundary
719-
[ErrorResponse]: https://api.reactrouter.com/v7/types/react_router.ErrorResponse.html
720-
[data]: ../api/utils/data

packages/react-router/__tests__/router/context-middleware-test.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ describe("context/middleware", () => {
418418
]);
419419
});
420420

421-
it("does not return result of middleware in client side routers", async () => {
421+
it("returns result of middleware in client side routers", async () => {
422422
let values: unknown[] = [];
423423
let consoleSpy = jest
424424
.spyOn(console, "warn")
@@ -434,12 +434,9 @@ describe("context/middleware", () => {
434434
path: "/parent",
435435
unstable_middleware: [
436436
async ({ context }, next) => {
437-
values.push(await next());
438-
return "NOPE";
439-
},
440-
async ({ context }, next) => {
441-
values.push(await next());
442-
return "NOPE";
437+
let results = await next();
438+
values.push({ ...results });
439+
return results;
443440
},
444441
],
445442
loader() {
@@ -451,16 +448,13 @@ describe("context/middleware", () => {
451448
path: "child",
452449
unstable_middleware: [
453450
async ({ context }, next) => {
454-
values.push(await next());
455-
return "NOPE";
456-
},
457-
async ({ context }, next) => {
458-
values.push(await next());
459-
return "NOPE";
451+
let results = await next();
452+
values.push({ ...results });
453+
return results;
460454
},
461455
],
462456
loader() {
463-
return values;
457+
return "CHILD";
464458
},
465459
},
466460
],
@@ -472,8 +466,18 @@ describe("context/middleware", () => {
472466

473467
expect(router.state.loaderData).toMatchObject({
474468
parent: "PARENT",
475-
child: [undefined, undefined, undefined, undefined],
469+
child: "CHILD",
476470
});
471+
expect(values).toEqual([
472+
{
473+
parent: { type: "data", result: "PARENT" },
474+
child: { type: "data", result: "CHILD" },
475+
},
476+
{
477+
parent: { type: "data", result: "PARENT" },
478+
child: { type: "data", result: "CHILD" },
479+
},
480+
]);
477481

478482
consoleSpy.mockRestore();
479483
});

0 commit comments

Comments
 (0)