Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-dots-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

[UNSTABLE] Update client middleware so it returns the data strategy results allowing for more advanced post-processing middleware
39 changes: 33 additions & 6 deletions docs/how-to/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,13 @@ export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
[serverMiddleware];
```

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:
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:

```ts
async function clientMiddleware({ request }, next) {
console.log(request.method, request.url);
await next(); // 👈 No return value
await next();
console.log(response.status, request.method, request.url);
// 👈 No need to return anything here
}

// Framework mode
Expand All @@ -279,6 +278,34 @@ const route = {
};
```

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.

Here's an example of the [CMS Redirect on 404][cms-redirect] use case implemented as a client side middleware:

```tsx
async function cmsFallbackMiddleware({ request }, next) {
const results = await next();

// Check if we got a 404 from any of our routes and if so, look for a
// redirect in our CMS
const found404 = Object.values(results).some(
(r) =>
isRouteErrorResponse(r.result) &&
r.result.status === 404,
);
if (found404) {
const cmsRedirect = await checkCMSRedirects(
request.url,
);
if (cmsRedirect) {
throw redirect(cmsRedirect, 302);
}
}
}
```

<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>

### When Middleware Runs

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

### 404 to CMS Redirect
### CMS Redirect on 404

```tsx filename=app/middleware/cms-fallback.ts
export const cmsFallbackMiddleware = async (
Expand Down Expand Up @@ -703,6 +730,8 @@ export async function loader({
[framework-action]: ../start/framework/route-module#action
[framework-loader]: ../start/framework/route-module#loader
[getloadcontext]: #changes-to-getloadcontextapploadcontext
[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
[cms-redirect]: #cms-redirect-on-404
[createContext]: ../api/utils/createContext
[RouterContextProvider]: ../api/utils/RouterContextProvider
[getContext]: ../api/data-routers/createBrowserRouter#optsunstable_getContext
Expand All @@ -716,5 +745,3 @@ export async function loader({
[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support
[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage
[ErrorBoundary]: ../start/framework/route-module#errorboundary
[ErrorResponse]: https://api.reactrouter.com/v7/types/react_router.ErrorResponse.html
[data]: ../api/utils/data
34 changes: 19 additions & 15 deletions packages/react-router/__tests__/router/context-middleware-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ describe("context/middleware", () => {
]);
});

it("does not return result of middleware in client side routers", async () => {
it("returns result of middleware in client side routers", async () => {
let values: unknown[] = [];
let consoleSpy = jest
.spyOn(console, "warn")
Expand All @@ -434,12 +434,9 @@ describe("context/middleware", () => {
path: "/parent",
unstable_middleware: [
async ({ context }, next) => {
values.push(await next());
return "NOPE";
},
async ({ context }, next) => {
values.push(await next());
return "NOPE";
let results = await next();
values.push({ ...results });
return results;
},
],
loader() {
Expand All @@ -451,16 +448,13 @@ describe("context/middleware", () => {
path: "child",
unstable_middleware: [
async ({ context }, next) => {
values.push(await next());
return "NOPE";
},
async ({ context }, next) => {
values.push(await next());
return "NOPE";
let results = await next();
values.push({ ...results });
return results;
},
],
loader() {
return values;
return "CHILD";
},
},
],
Expand All @@ -472,8 +466,18 @@ describe("context/middleware", () => {

expect(router.state.loaderData).toMatchObject({
parent: "PARENT",
child: [undefined, undefined, undefined, undefined],
child: "CHILD",
});
expect(values).toEqual([
{
parent: { type: "data", result: "PARENT" },
child: { type: "data", result: "CHILD" },
},
{
parent: { type: "data", result: "PARENT" },
child: { type: "data", result: "CHILD" },
},
]);

consoleSpy.mockRestore();
});
Expand Down
Loading