|
| 1 | +--- |
| 2 | +"react-router": patch |
| 3 | +--- |
| 4 | + |
| 5 | +Support middleware on routes (unstable) |
| 6 | + |
| 7 | +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: |
| 8 | + |
| 9 | +```ts |
| 10 | +import type { Config } from "@react-router/dev/config"; |
| 11 | +import type { Future } from "react-router"; |
| 12 | + |
| 13 | +declare module "react-router" { |
| 14 | + interface Future { |
| 15 | + unstable_middleware: true; // 👈 Enable middleware types |
| 16 | + } |
| 17 | +} |
| 18 | + |
| 19 | +export default { |
| 20 | + future: { |
| 21 | + unstable_middleware: true, // 👈 Enable middleware |
| 22 | + }, |
| 23 | +} satisfies Config; |
| 24 | +``` |
| 25 | + |
| 26 | +⚠️ 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. |
| 27 | + |
| 28 | +⚠️ Enabling middleware contains a breaking change to the `context` parameter passed to your `loader`/`action` functions - see below for more information. |
| 29 | + |
| 30 | +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. |
| 31 | + |
| 32 | +```tsx |
| 33 | +// Framework mode |
| 34 | +export const unstable_middleware = [serverLogger, serverAuth]; // server |
| 35 | +export const unstable_clientMiddleware = [clientLogger]; // client |
| 36 | + |
| 37 | +// Library mode |
| 38 | +const routes = [ |
| 39 | + { |
| 40 | + path: "/", |
| 41 | + // Middlewares are client-side for library mode SPA's |
| 42 | + unstable_middleware: [clientLogger, clientAuth], |
| 43 | + loader: rootLoader, |
| 44 | + Component: Root, |
| 45 | + }, |
| 46 | +]; |
| 47 | +``` |
| 48 | + |
| 49 | +Here's a simple example of a client-side logging middleware that can be placed on the root route: |
| 50 | + |
| 51 | +```tsx |
| 52 | +const clientLogger: Route.unstable_ClientMiddlewareFunction = async ( |
| 53 | + { request }, |
| 54 | + next |
| 55 | +) => { |
| 56 | + let start = performance.now(); |
| 57 | + |
| 58 | + // Run the remaining middlewares and all route loaders |
| 59 | + await next(); |
| 60 | + |
| 61 | + let duration = performance.now() - start; |
| 62 | + console.log(`Navigated to ${request.url} (${duration}ms)`); |
| 63 | +}; |
| 64 | +``` |
| 65 | + |
| 66 | +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`. |
| 67 | + |
| 68 | +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()`. |
| 69 | + |
| 70 | +```tsx |
| 71 | +const serverLogger: Route.unstable_MiddlewareFunction = async ( |
| 72 | + { request, params, context }, |
| 73 | + next |
| 74 | +) => { |
| 75 | + let start = performance.now(); |
| 76 | + |
| 77 | + // 👇 Grab the response here |
| 78 | + let res = await next(); |
| 79 | + |
| 80 | + let duration = performance.now() - start; |
| 81 | + console.log(`Navigated to ${request.url} (${duration}ms)`); |
| 82 | + |
| 83 | + // 👇 And return it here (optional if you don't modify the response) |
| 84 | + return res; |
| 85 | +}; |
| 86 | +``` |
| 87 | + |
| 88 | +You can throw a `redirect` from a middleware to short circuit any remaining processing: |
| 89 | + |
| 90 | +```tsx |
| 91 | +import { sessionContext } from "../context"; |
| 92 | +const serverAuth: Route.unstable_MiddlewareFunction = ( |
| 93 | + { request, params, context }, |
| 94 | + next |
| 95 | +) => { |
| 96 | + let session = context.get(sessionContext); |
| 97 | + let user = session.get("user"); |
| 98 | + if (!user) { |
| 99 | + session.set("returnTo", request.url); |
| 100 | + throw redirect("/login", 302); |
| 101 | + } |
| 102 | +}; |
| 103 | +``` |
| 104 | + |
| 105 | +_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`._ |
| 106 | + |
| 107 | +Here's another example of using a server middleware to detect 404s and check the CMS for a redirect: |
| 108 | + |
| 109 | +```tsx |
| 110 | +const redirects: Route.unstable_MiddlewareFunction = async ({ |
| 111 | + request, |
| 112 | + next, |
| 113 | +}) => { |
| 114 | + // attempt to handle the request |
| 115 | + let res = await next(); |
| 116 | + |
| 117 | + // if it's a 404, check the CMS for a redirect, do it last |
| 118 | + // because it's expensive |
| 119 | + if (res.status === 404) { |
| 120 | + let cmsRedirect = await checkCMSRedirects(request.url); |
| 121 | + if (cmsRedirect) { |
| 122 | + throw redirect(cmsRedirect, 302); |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + return res; |
| 127 | +}; |
| 128 | +``` |
| 129 | + |
| 130 | +**`context` parameter** |
| 131 | + |
| 132 | +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`): |
| 133 | + |
| 134 | +```ts |
| 135 | +import { unstable_createContext } from "react-router"; |
| 136 | +import { Route } from "./+types/root"; |
| 137 | +import type { Session } from "./sessions.server"; |
| 138 | +import { getSession } from "./sessions.server"; |
| 139 | + |
| 140 | +let sessionContext = unstable_createContext<Session>(); |
| 141 | + |
| 142 | +const sessionMiddleware: Route.unstable_MiddlewareFunction = ({ |
| 143 | + context, |
| 144 | + request, |
| 145 | +}) => { |
| 146 | + let session = await getSession(request); |
| 147 | + context.set(sessionContext, session); |
| 148 | + // ^ must be of type Session |
| 149 | +}; |
| 150 | + |
| 151 | +// ... then in some downstream middleware |
| 152 | +const loggerMiddleware: Route.unstable_MiddlewareFunction = ({ |
| 153 | + context, |
| 154 | + request, |
| 155 | +}) => { |
| 156 | + let session = context.get(sessionContext); |
| 157 | + // ^ typeof Session |
| 158 | + console.log(session.get("userId"), request.method, request.url); |
| 159 | +}; |
| 160 | + |
| 161 | +// ... or some downstream loader |
| 162 | +export function loader({ context }: Route.LoaderArgs) { |
| 163 | + let session = context.get(sessionContext); |
| 164 | + let profile = await getProfile(session.get("userId")); |
| 165 | + return { profile }; |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +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>`): |
| 170 | + |
| 171 | +```ts |
| 172 | +let adapterContext = unstable_createContext<MyAdapterContext>(); |
| 173 | + |
| 174 | +function getLoadContext(req, res): unstable_InitialContext { |
| 175 | + let map = new Map(); |
| 176 | + map.set(adapterContext, getAdapterContext(req)); |
| 177 | + return map; |
| 178 | +} |
| 179 | +``` |
0 commit comments