getLoadContext
函数(如果适用)unstable_getContext()
函数(可选)next
函数next()
next()
和错误处理getLoadContext
/AppLoadContext
的更改AppLoadContext
迁移future.unstable_middleware
标志来启用它。
中间件允许你在为匹配路径生成 Response
之前和之后运行代码。这使得像身份验证、日志记录、错误处理和数据预处理等常见模式能够以可重用的方式实现。
中间件以嵌套链的方式运行,在“向下”到你的路由处理程序时,从父路由执行到子路由,然后在生成 Response
后“向上”时,从子路由返回到父路由。
例如,对于一个 GET /parent/child
请求,中间件将按以下顺序运行:
- Root middleware start
- Parent middleware start
- Child middleware start
- Run loaders, generate HTML Response
- Child middleware end
- Parent middleware end
- Root middleware end
首先,在你的 React Router 配置中启用中间件:
import type { Config } from "@react-router/dev/config";
export default {
future: {
unstable_middleware: true,
},
} satisfies Config;
context
参数类型。如果你目前正在使用 context
,请注意下面关于 getLoadContext 的部分。
中间件使用 context
提供者实例来向下游的中间件链提供数据。你可以使用 unstable_createContext
创建类型安全的上下文对象:
import { unstable_createContext } from "react-router";
import type { User } from "~/types";
export const userContext =
unstable_createContext<User | null>(null);
import { redirect } from "react-router";
import { userContext } from "~/context";
// Server-side Authentication Middleware
async function authMiddleware({ request, context }) {
const user = await getUserFromSession(request);
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
}
export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
[authMiddleware];
// Client-side timing middleware
async function timingMiddleware({ context }, next) {
const start = performance.now();
await next();
const duration = performance.now() - start;
console.log(`Navigation took ${duration}ms`);
}
export const unstable_clientMiddleware: Route.unstable_ClientMiddlewareFunction[] =
[timingMiddleware];
export async function loader({
context,
}: Route.LoaderArgs) {
const user = context.get(userContext);
const profile = await getProfile(user);
return { profile };
}
export default function Dashboard({
loaderData,
}: Route.ComponentProps) {
return (
<div>
<h1>Welcome {loaderData.profile.fullName}!</h1>
<Profile profile={loaderData.profile} />
</div>
);
}
getLoadContext
函数(如果适用)如果你正在使用自定义服务器和 getLoadContext
函数,你需要更新你的实现以返回一个 unstable_RouterContextProvider
的实例,而不是一个 JavaScript 对象:
+import {
+ unstable_createContext,
+ unstable_RouterContextProvider,
+} from "react-router";
import { createDb } from "./db";
+const dbContext = unstable_createContext<Database>();
function getLoadContext(req, res) {
- return { db: createDb() };
+ const context = new unstable_RouterContextProvider();
+ context.set(dbContext, createDb());
+ return context;
}
const router = createBrowserRouter(routes, {
future: {
unstable_middleware: true,
},
});
中间件使用 context
提供者实例来向下游的中间件链提供数据。你可以使用 unstable_createContext
创建类型安全的上下文对象:
import { unstable_createContext } from "react-router";
import type { User } from "~/types";
export const userContext =
unstable_createContext<User | null>(null);
import { redirect } from "react-router";
import { userContext } from "~/context";
const routes = [
{
path: "/",
unstable_middleware: [timingMiddleware], // 👈
Component: Root,
children: [
{
path: "profile",
unstable_middleware: [authMiddleware], // 👈
loader: profileLoader,
Component: Profile,
},
{
path: "login",
Component: Login,
},
],
},
];
async function timingMiddleware({ context }, next) {
const start = performance.now();
await next();
const duration = performance.now() - start;
console.log(`Navigation took ${duration}ms`);
}
async function authMiddleware({ context }) {
const user = await getUser();
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
}
export async function profileLoader({
context,
}: Route.LoaderArgs) {
const user = context.get(userContext);
const profile = await getProfile(user);
return { profile };
}
export default function Profile() {
let loaderData = useLoaderData();
return (
<div>
<h1>Welcome {loaderData.profile.fullName}!</h1>
<Profile profile={loaderData.profile} />
</div>
);
}
unstable_getContext()
函数(可选)如果你希望在所有导航/获取操作中包含一个基础上下文,可以向你的路由器添加一个 unstable_getContext
函数。每次导航/获取时都会调用此函数来填充一个新的上下文。
let sessionContext = unstable_createContext();
const router = createBrowserRouter(routes, {
future: {
unstable_middleware: true,
},
unstable_getContext() {
let context = new unstable_RouterContextProvider();
context.set(sessionContext, getSession());
return context;
},
});
getLoadContext
API,该 API 用于将值从你的 HTTP 服务器传递给 React Router 处理程序。这个 unstable_getContext
API 可用于将 window
/document
中的全局值传递给 React Router,但由于它们都在同一个上下文(浏览器)中运行,你实际上可以通过根路由中间件实现相同的行为。因此,你可能不像在服务器上那样需要这个 API——但为了保持一致性,我们提供了它。
服务器中间件在框架模式下的服务器上运行,用于处理 HTML 文档请求和后续导航及 fetcher 调用的 .data
请求。因为服务器中间件是在服务器上响应 HTTP Request
运行的,所以它通过 next
函数将 HTTP Response
返回到中间件链的上层:
async function serverMiddleware({ request }, next) {
console.log(request.method, request.url);
let response = await next();
console.log(response.status, request.method, request.url);
return response;
}
// Framework mode only
export const unstable_middleware = [serverMiddleware];
客户端中间件在浏览器中的框架模式和数据模式下运行,用于客户端导航和 fetcher 调用。客户端中间件的不同之处在于没有 HTTP 请求,因此它不会通过 next
函数冒泡任何东西:
async function clientMiddleware({ request }, next) {
console.log(request.method, request.url);
await next(); // 👈 No return value
console.log(response.status, request.method, request.url);
// 👈 No need to return anything here
}
// Framework mode
export const unstable_clientMiddleware = [clientMiddleware];
// Or, Data mode
const route = {
path: "/",
unstable_middleware: [clientMiddleware],
loader: rootLoader,
Component: Root,
};
了解你的中间件何时运行非常重要,以确保你的应用程序按预期运行。
在一个已注水的框架模式应用中,服务器中间件的设计优先考虑 SPA 行为,默认情况下不会创建新的网络活动。中间件包装现有请求,并且仅在您需要访问服务器时运行。
这就提出了一个问题:在 React Router 中,“处理程序”是什么?是路由吗?还是 loader?我们认为“视情况而定”。
GET /route
)中,处理程序是路由——因为响应同时包含了 loader 和路由组件。GET /route.data
)中,处理程序是 loader
/action
,因为响应中只包含这些内容。因此:
.data
请求以获取 loader
/action
时才会运行服务器中间件。对于请求注释中间件,如记录请求时长、检查/设置会话、设置传出缓存头等,这种行为非常重要。如果本来就没有理由访问服务器,那么去服务器运行这些类型的中间件将是无用的。这会导致服务器负载增加和服务器日志嘈杂。
// This middleware won't run on client-side navigations without a `.data` request
function loggingMiddleware({ request }, next) {
console.log(`Request: ${request.method} ${request.url}`);
let response = await next();
console.log(
`Response: ${response.status} ${request.method} ${request.url}`,
);
return response;
}
export const unstable_middleware = [loggingMiddleware];
然而,在某些情况下,你可能希望在每次客户端导航时都运行某些服务器中间件——即使没有 loader 存在。例如,在你的网站的已认证部分有一个表单,它不需要 loader
,但你宁愿使用身份验证中间件在用户填写表单之前就将他们重定向——而不是在他们提交到 action
时。如果你的中间件符合这个标准,那么你可以在包含该中间件的路由上放置一个 loader
,以强制它在涉及该路由的客户端导航中总是调用服务器。
function authMiddleware({ request }, next) {
if (!isLoggedIn(request)) {
throw redirect("/login");
}
}
export const unstable_middleware = [authMiddleware];
// By adding a loader, we force the authMiddleware to run on every client-side
// navigation involving this route.
export function loader() {
return null;
}
客户端中间件更简单,因为我们已经在客户端,并且在导航时总是向路由器发出“请求”。客户端中间件将在每次客户端导航时运行,无论是否有要运行的 loaders。
新的上下文系统提供了类型安全,并防止了命名冲突,允许你向嵌套的中间件和 loader/action 函数提供数据。在框架模式下,这取代了之前的 AppLoadContext
API。
// ✅ Type-safe
import { unstable_createContext } from "react-router";
const userContext = unstable_createContext<User>();
// Later in middleware/loaders
context.set(userContext, user); // Must be User type
const user = context.get(userContext); // Returns User type
// ❌ Old way (no type safety)
// context.user = user; // Could be anything
Node 提供了一个 AsyncLocalStorage
API,它提供了一种通过异步执行上下文传递值的方式。虽然这是一个 Node API,但大多数现代运行时都已经(大部分)使其可用(例如,Cloudflare、Bun、Deno)。
理论上,我们本可以直接利用 AsyncLocalStorage
作为从中间件向子路由传递值的方式,但缺乏 100% 的跨平台兼容性令人担忧,因此我们仍然希望提供一个一流的 context
API,以便有一种发布可重用的中间件包的方式,保证以运行时无关的方式工作。
也就是说,这个 API 仍然可以很好地与 React Router 中间件配合使用,并且可以替代 context
API 或与之并用。
AsyncLocalStorage
尤其强大,因为它允许你从 middleware
向你的服务器组件和服务器操作提供信息,因为它们在同一个服务器执行上下文中运行 🤯
import { AsyncLocalStorage } from "node:async_hooks";
const USER = new AsyncLocalStorage<User>();
export async function provideUser(
request: Request,
cb: () => Promise<Response>,
) {
let user = await getUser(request);
return USER.run(user, cb);
}
export function getUser() {
return USER.getStore();
}
import { provideUser } from "./user-context";
export const unstable_middleware: Route.unstable_MiddlewareFunction[] =
[
async ({ request, context }, next) => {
return provideUser(request, async () => {
let res = await next();
return res;
});
},
];
import { getUser } from "../user-context";
export function loader() {
let user = getUser();
//...
}
next
函数next
函数的逻辑取决于它从哪个路由中间件中被调用:
Response
。const middleware = async ({ context }, next) => {
// Code here runs BEFORE handlers
console.log("Before");
const response = await next();
// Code here runs AFTER handlers
console.log("After");
return response; // Optional on client, required on server
};
next()
。多次调用会抛出错误。
next()
如果你不需要在处理程序之后运行代码,可以跳过调用 next()
。
const authMiddleware = async ({ request, context }) => {
const user = await getUser(request);
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
// next() is called automatically
};
next()
和错误处理React Router 通过路由的 ErrorBoundary
导出提供了内置的错误处理。就像 loader/action 抛出错误时一样(基本上),如果一个中间件抛出错误,它将被相应的 ErrorBoundary
捕获和处理,并且一个 Response
将通过祖先的 next()
调用返回。这意味着 next()
函数永远不应该抛出错误,并且应该总是返回一个 Response
,所以你不需要担心将它包装在 try/catch 中。
这种行为对于允许中间件模式非常重要,例如从根中间件自动为传出的响应设置必需的标头(例如,提交一个会话)。如果来自中间件的任何错误导致 next()
throw
,我们就会错过在返回时执行祖先中间件,而那些必需的标头就不会被设置。
// routes/parent.tsx
export const unstable_middleware = [
async (_, next) => {
let res = await next();
// ^ res.status = 500
// This response contains the ErrorBoundary
return res;
}
]
// routes/parent.child.tsx
export const unstable_middleware = [
async (_, next) => {
let res = await next();
// ^ res.status = 200
// This response contains the successful UI render
throw new Error('Uh oh, something went wrong!)
}
]
loader
/action
中抛出非重定向的 Response
与在 middleware
中抛出 Response
(重定向行为相同)之间有一个微小而微妙的区别。
从中间件抛出非重定向响应将直接使用该响应,并通过父级的 next()
调用返回它。这与 loaders
/actions
中的行为不同,在后者中,该响应将被转换为一个 ErrorResponse
,由 ErrorBoundary
渲染。
这里的区别在于,loaders
/actions
期望返回数据,然后提供给组件进行渲染。但中间件期望返回一个响应——所以如果你返回或抛出一个响应,我们将直接使用它。如果你想从中间件向错误边界抛出一个带有状态码的错误,你应该使用 data
工具。
getLoadContext
/AppLoadContext
的更改getLoadContext
函数的情况。
中间件对由 getLoadContext
生成并传递给你的 loaders 和 actions 的 context
参数引入了一个破坏性变更。当前通过模块增强 AppLoadContext
的方法并不是真正的类型安全,而只是告诉 TypeScript“相信我”。
中间件在客户端需要一个等效的 context
用于 clientMiddleware
,但我们不想复制服务器上我们已经不太满意的这种模式,所以我们决定引入一个新的 API,以便我们可以解决类型安全问题。
选择使用中间件时,context
参数会变为 RouterContextProvider
的一个实例。
let dbContext = unstable_createContext<Database>();
let context = new unstable_RouterContextProvider();
context.set(dbContext, getDb());
// ^ type-safe
let db = context.get(dbContext);
// ^ Database
如果你正在使用自定义服务器和 getLoadContext
函数,你需要更新你的实现,以返回一个 unstable_RouterContextProvider
的实例,而不是一个普通的 JavaScript 对象。
+import {
+ unstable_createContext,
+ unstable_RouterContextProvider,
+} from "react-router";
import { createDb } from "./db";
+const dbContext = unstable_createContext<Database>();
function getLoadContext(req, res) {
- return { db: createDb() };
+ const context = new unstable_RouterContextProvider();
+ context.set(dbContext, createDb());
+ return context;
}
AppLoadContext
迁移如果你当前正在使用 AppLoadContext
,你可以通过使用现有的模块增强来增强 unstable_RouterContextProvider
而不是 AppLoadContext
,从而进行增量迁移。然后,更新你的 getLoadContext
函数以返回一个 unstable_RouterContextProvider
的实例。
declare module "react-router" {
- interface AppLoadContext {
+ interface unstable_RouterContextProvider {
db: Database;
user: User;
}
}
function getLoadContext() {
const loadContext = {...};
- return loadContext;
+ let context = new unstable_RouterContextProvider();
+ Object.assign(context, loadContext);
+ return context;
}
这允许你在初次采用中间件时保持你的 loaders/actions 不变,因为它们仍然可以直接读取值(即 context.db
)。
context.set
/context.get
。不能保证这种方法在 React Router 的下一个主要版本中仍然有效。
unstable_RouterContextProvider
类也用于客户端的 context
参数,通过 <HydratedRouter unstable_getContext>
和 <RouterProvider unstable_getContext>
。由于 AppLoadContext
主要用作从你的 HTTP 服务器到 React Router 处理程序的传递,你需要注意,即使 TypeScript 会告诉你这些增强字段可用,它们在 clientMiddleware
、clientLoader
或 clientAction
函数中将不可用(除非,当然,你通过客户端的 unstable_getContext
提供了这些字段)。
import { redirect } from "react-router";
import { userContext } from "~/context";
import { getSession } from "~/sessions.server";
export const authMiddleware = async ({
request,
context,
}) => {
const session = await getSession(request);
const userId = session.get("userId");
if (!userId) {
throw redirect("/login");
}
const user = await getUserById(userId);
context.set(userContext, user);
};
import { authMiddleware } from "~/middleware/auth";
export const unstable_middleware = [authMiddleware];
export function loader({ context }: Route.LoaderArgs) {
const user = context.get(userContext); // Guaranteed to exist
return { user };
}
import { requestIdContext } from "~/context";
export const loggingMiddleware = async (
{ request, context },
next,
) => {
const requestId = crypto.randomUUID();
context.set(requestIdContext, requestId);
console.log(
`[${requestId}] ${request.method} ${request.url}`,
);
const start = performance.now();
const response = await next();
const duration = performance.now() - start;
console.log(
`[${requestId}] Response ${response.status} (${duration}ms)`,
);
return response;
};
export const cmsFallbackMiddleware = async (
{ request },
next,
) => {
const response = await next();
// Check if we got a 404
if (response.status === 404) {
// Check CMS for a redirect
const cmsRedirect = await checkCMSRedirects(
request.url,
);
if (cmsRedirect) {
throw redirect(cmsRedirect, 302);
}
}
return response;
};
export const headersMiddleware = async (
{ context },
next,
) => {
const response = await next();
// Add security headers
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
return response;
};
export const unstable_middleware = [
async ({ request, context }, next) => {
// Only run auth for POST requests
if (request.method === "POST") {
await ensureAuthenticated(request, context);
}
return next();
},
];
const sharedDataContext = unstable_createContext<any>();
export const unstable_middleware = [
async ({ request, context }, next) => {
if (request.method === "POST") {
// Set data during action phase
context.set(
sharedDataContext,
await getExpensiveData(),
);
}
return next();
},
];
export async function action({
context,
}: Route.ActionArgs) {
const data = context.get(sharedDataContext);
// Use the data...
}
export async function loader({
context,
}: Route.LoaderArgs) {
const data = context.get(sharedDataContext);
// Same data is available here
}