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