Support middleware on routes (unstable) (#12941)
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:
import type { Config } from "@react-router/dev/config";
import type { Future } from "react-router";
declare module "react-router" {
interface Future {
unstable_middleware: true;
}
}
export default {
future: {
unstable_middleware: true,
},
} 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.
export const unstable_middleware = [serverLogger, serverAuth];
export const unstable_clientMiddleware = [clientLogger];
const routes = [
{
path: "/",
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:
const clientLogger: Route.unstable_ClientMiddlewareFunction = async (
{ request },
next,
) => {
let start = performance.now();
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().
const serverLogger: Route.unstable_MiddlewareFunction = async (
{ request, params, context },
next,
) => {
let start = performance.now();
let res = await next();
let duration = performance.now() - start;
console.log(`Navigated to ${request.url} (${duration}ms)`);
return res;
};
You can throw a redirect from a middleware to short circuit any remaining processing:
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:
const redirects: Route.unstable_MiddlewareFunction = async ({
request,
next,
}) => {
let res = await next();
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):
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);
};
const loggerMiddleware: Route.unstable_MiddlewareFunction = ({
context,
request,
}) => {
let session = context.get(sessionContext);
console.log(session.get("userId"), request.method, request.url);
};
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>):
let adapterContext = unstable_createContext<MyAdapterContext>();
function getLoadContext(req, res): unstable_InitialContext {
let map = new Map();
map.set(adapterContext, getAdapterContext(req));
return map;
}
Add context support to client side data routers (unstable) (#12941)
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:
import { unstable_createContext } from "react-router";
type User = {
};
let userContext = unstable_createContext<User>();
function sessionMiddleware({ context }) {
let user = await getUser();
context.set(userContext, user);
}
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:
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;
}