Skip to content

Add support for client context and middleware (unstable) #12941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6e7ac3e
Support for client side context (#12515)
brophdawg11 Jan 22, 2025
d7ac75b
Initial middleware implementation (#12810)
brophdawg11 Jan 23, 2025
6eb4a47
Scope client side context to the navigation or fetch (#12856)
brophdawg11 Jan 24, 2025
a57d971
Middleware code cleanup (#12884)
brophdawg11 Jan 30, 2025
2bca471
Add unstable to public API (#12917)
brophdawg11 Jan 30, 2025
a383e1e
Fix up a few type issues in tests
brophdawg11 Jan 30, 2025
ea71e74
Updates
brophdawg11 Feb 3, 2025
1ea21fd
Add future flag
brophdawg11 Feb 4, 2025
658a4f8
Merge branch 'dev' into brophdawg11/feat-middleware
brophdawg11 Feb 20, 2025
db70549
Enable middleware on new non-SSR flows
brophdawg11 Feb 24, 2025
ee4ab53
Move next to second parameter
brophdawg11 Feb 24, 2025
4ffbf90
Fix typos in docs
brophdawg11 Feb 26, 2025
d34b58f
Add playground
brophdawg11 Feb 26, 2025
8c2d6d3
Adopt new ContexProvider API
brophdawg11 Feb 26, 2025
f174d2f
Merge branch 'dev' into brophdawg11/feat-middleware
brophdawg11 Feb 27, 2025
d211408
Fix playfground
brophdawg11 Feb 27, 2025
10a10ef
Fix return type of client middleware fn
brophdawg11 Feb 27, 2025
5322426
Updates to playground
brophdawg11 Feb 27, 2025
687bb87
fix lint/test issues
brophdawg11 Feb 28, 2025
bcbd8f7
Update getLoadContext types for middleware context
brophdawg11 Feb 28, 2025
60ca9bc
Update playground to TS and apply getLoadContext
brophdawg11 Feb 28, 2025
4f9d351
Fix unit test
brophdawg11 Feb 28, 2025
d499f66
Updates
brophdawg11 Feb 28, 2025
a028b71
womp womp lint fix
brophdawg11 Feb 28, 2025
f238c24
Add perf note for unstable release
brophdawg11 Feb 28, 2025
cb8287b
Update docs to remove invalid satisfies usage
brophdawg11 Feb 28, 2025
98c0b59
Bubble next result if not returned
brophdawg11 Feb 28, 2025
23ff92a
Fix vite dev getLoadContext type
brophdawg11 Mar 1, 2025
23f9f4e
Add note on getLoadContext to changeset
brophdawg11 Mar 1, 2025
6b61caa
Fix up some missed AppLoadContext types
brophdawg11 Mar 3, 2025
a4ed390
Remove context generic from MiddlewareFunction
brophdawg11 Mar 3, 2025
63fb95b
Update decision doc with unstable_ prefixes
brophdawg11 Mar 3, 2025
979cf6c
Merge branch 'dev' into brophdawg11/feat-middleware
brophdawg11 Mar 3, 2025
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
179 changes: 179 additions & 0 deletions .changeset/middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
---
"react-router": patch
---

Support middleware on routes (unstable)

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:

```ts
import type { Config } from "@react-router/dev/config";
import type { Future } from "react-router";

declare module "react-router" {
interface Future {
unstable_middleware: true; // 👈 Enable middleware types
}
}

export default {
future: {
unstable_middleware: true, // 👈 Enable middleware
},
} satisfies Config;
```

⚠️ 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.

⚠️ Enabling middleware contains a breaking change to the `context` parameter passed to your `loader`/`action` functions - see below for more information.

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.

```tsx
// Framework mode
export const unstable_middleware = [serverLogger, serverAuth]; // server
export const unstable_clientMiddleware = [clientLogger]; // client

// Library mode
const routes = [
{
path: "/",
// Middlewares are client-side for library mode SPA's
unstable_middleware: [clientLogger, clientAuth],
loader: rootLoader,
Component: Root,
},
];
```

Here's a simple example of a client-side logging middleware that can be placed on the root route:

```tsx
const clientLogger: Route.unstable_ClientMiddlewareFunction = async (
{ request },
next
) => {
let start = performance.now();

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

let duration = performance.now() - start;
console.log(`Navigated to ${request.url} (${duration}ms)`);
};
```

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

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

```tsx
const serverLogger: Route.unstable_MiddlewareFunction = async (
{ request, params, context },
next
) => {
let start = performance.now();

// 👇 Grab the response here
let res = await next();

let duration = performance.now() - start;
console.log(`Navigated to ${request.url} (${duration}ms)`);

// 👇 And return it here (optional if you don't modify the response)
return res;
};
```

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

```tsx
import { sessionContext } from "../context";
const serverAuth: Route.unstable_MiddlewareFunction = (
{ request, params, context },
next
) => {
let session = context.get(sessionContext);
let user = session.get("user");
if (!user) {
session.set("returnTo", request.url);
throw redirect("/login", 302);
}
};
```

_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`._

Here's another example of using a server middleware to detect 404s and check the CMS for a redirect:

```tsx
const redirects: Route.unstable_MiddlewareFunction = async ({
request,
next,
}) => {
// attempt to handle the request
let res = await next();

// if it's a 404, check the CMS for a redirect, do it last
// because it's expensive
if (res.status === 404) {
let cmsRedirect = await checkCMSRedirects(request.url);
if (cmsRedirect) {
throw redirect(cmsRedirect, 302);
}
}

return res;
};
```

**`context` parameter**

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

```ts
import { unstable_createContext } from "react-router";
import { Route } from "./+types/root";
import type { Session } from "./sessions.server";
import { getSession } from "./sessions.server";

let sessionContext = unstable_createContext<Session>();

const sessionMiddleware: Route.unstable_MiddlewareFunction = ({
context,
request,
}) => {
let session = await getSession(request);
context.set(sessionContext, session);
// ^ must be of type Session
};

// ... then in some downstream middleware
const loggerMiddleware: Route.unstable_MiddlewareFunction = ({
context,
request,
}) => {
let session = context.get(sessionContext);
// ^ typeof Session
console.log(session.get("userId"), request.method, request.url);
};

// ... or some downstream loader
export function loader({ context }: Route.LoaderArgs) {
let session = context.get(sessionContext);
let profile = await getProfile(session.get("userId"));
return { profile };
}
```

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

```ts
let adapterContext = unstable_createContext<MyAdapterContext>();

function getLoadContext(req, res): unstable_InitialContext {
let map = new Map();
map.set(adapterContext, getAdapterContext(req));
return map;
}
```
50 changes: 50 additions & 0 deletions .changeset/spa-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
"react-router": patch
---

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

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:

```ts
import { unstable_createContext } from "react-router";

type User = {
/*...*/
};

let userContext = unstable_createContext<User>();

function sessionMiddleware({ context }) {
let user = await getUser();
context.set(userContext, user);
}

// ... then in some downstream loader
function loader({ context }) {
let user = context.get(userContext);
let profile = await getProfile(user.id);
return { profile };
}
```

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:

- Library mode - `createBrowserRouter(routes, { unstable_getContext })`
- Framework mode - `<HydratedRouter unstable_getContext>`

This function should return an value of type `unstable_InitialContext` which is a `Map<unstable_RouterContext, unknown>` of context's and initial values:

```ts
const loggerContext = unstable_createContext<(...args: unknown[]) => void>();

function logger(...args: unknown[]) {
console.log(new Date.toISOString(), ...args);
}

function unstable_getContext() {
let map = new Map();
map.set(loggerContext, logger);
return map;
}
```
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ packages/react-router-dom/server.d.ts
packages/react-router-dom/server.js
packages/react-router-dom/server.mjs
tutorial/dist/
public/
Loading