React Router 在 v7 版本中融合了 Remix 的全栈能力,演进为支持服务端渲染的全栈 Web 框架。它全面基于 Web Standard API(Request / Response / FormData)构建,内置了数据加载与突变状态机、全链路类型自动生成以及嵌套路由体系。
配置 React Router v7 项目并开启 SSR 模式。
参考文档:
npm i react-router @react-router/node @react-router/serve isbot
npm i -D @react-router/dev vite typescript @types/node @types/react @types/react-dom
编辑 react-router.config.ts 开启服务端渲染:
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
} satisfies Config;
编辑 app/routes.ts,支持集中式声明路由、嵌套布局与动态路由参数:
import {
type RouteConfig,
index,
route,
layout,
} from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
layout("routes/layout.app.tsx", [
route("dashboard", "routes/dashboard.tsx"),
route("courses", "routes/courses.tsx"),
route("courses/:slug", "routes/courses.$slug.tsx"),
route("admin/categories", "routes/admin.categories.tsx"),
]),
route("login", "routes/login.tsx"),
route("api/logout", "routes/api.logout.ts"),
] satisfies RouteConfig;
编辑 app/root.tsx 组织服务端直出结构:
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import type { Route } from "./+types/root";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
在服务端 Loader 中直接读取数据库(如 Drizzle ORM),并通过 Typegen 获取强类型推导。
参考文档:
在 tsconfig.json 中配置编译路径映射:
{
"compilerOptions": {
"rootDirs": [".", "./.react-router/types"]
}
}
框架会基于路由文件自动生成 ./+types/<route-name>.ts。
在路由组件中导出 loader 函数与页面组件:
import type { Route } from "./+types/courses.$slug";
import { db } from "~/db";
import { coursesTable } from "~/db/schema";
import { eq } from "drizzle-orm";
import { data } from "react-router";
export async function loader({ params }: Route.LoaderArgs) {
const { slug } = params;
const course = await db.query.coursesTable.findFirst({
where: eq(coursesTable.slug, slug),
});
if (!course) {
throw data("课程不存在", { status: 404 });
}
return { course };
}
export default function CourseDetail({ loaderData }: Route.ComponentProps) {
const { course } = loaderData;
return (
<main>
<h1>{course.title}</h1>
<p>{course.description}</p>
</main>
);
}
处理客户端提交的数据变更,区分为页面级导航表单与局部独立突变。
参考文档:
<Form>:页面级提交,触发生命周期导航,更新 URL 与浏览器历史记录。useFetcher:局部独立提交,URL 保持不变,支持并发实例与组件级状态隔离。在单一路由中处理多种操作(如创建、更新、删除),通过 intent 字段与 Zod discriminatedUnion 进行强类型分发:
import { useFetcher, data } from "react-router";
import { z } from "zod";
import type { Route } from "./+types/admin.categories";
import { db } from "~/db";
import { categoriesTable } from "~/db/schema";
import { eq } from "drizzle-orm";
const categoryActionSchema = z.discriminatedUnion("intent", [
z.object({
intent: z.literal("create"),
name: z.string().trim().min(1, "分类名称不能为空"),
}),
z.object({
intent: z.literal("update"),
categoryId: z.coerce.number().int(),
name: z.string().trim().min(1, "分类名称不能为空"),
}),
z.object({
intent: z.literal("delete"),
categoryId: z.coerce.number().int(),
}),
]);
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const submission = Object.fromEntries(formData);
const parsed = categoryActionSchema.safeParse(submission);
if (!parsed.success) {
return data(
{ error: parsed.error.issues[0]?.message ?? "输入数据无效" },
{ status: 400 }
);
}
const payload = parsed.data;
switch (payload.intent) {
case "create": {
await db.insert(categoriesTable).values({ name: payload.name });
return { success: true, message: "分类创建成功" };
}
case "update": {
await db
.update(categoriesTable)
.set({ name: payload.name })
.where(eq(categoriesTable.id, payload.categoryId));
return { success: true, message: "分类更新成功" };
}
case "delete": {
await db
.delete(categoriesTable)
.where(eq(categoriesTable.id, payload.categoryId));
return { success: true, message: "分类已删除" };
}
}
}
export default function AdminCategories() {
const createFetcher = useFetcher<typeof action>();
const deleteFetcher = useFetcher<typeof action>();
return (
<div>
<createFetcher.Form method="post">
<input type="hidden" name="intent" value="create" />
<input name="name" placeholder="新建分类名称" />
<button
type="submit"
disabled={createFetcher.state === "submitting"}
>
{createFetcher.state === "submitting" ? "提交中..." : "新增"}
</button>
</createFetcher.Form>
{createFetcher.data?.error && (
<p className="error">{createFetcher.data.error}</p>
)}
</div>
);
}
Action 成功执行后,React Router 默认触发活动 Loader 并行重校验,确保服务端与客户端数据状态一致。
参考文档:
action 执行完毕后,当前页面匹配的所有激活 loader 会被自动重新调用。对于高频操作(如实时草稿保存、点赞或只影响局部的数据突变),可通过 shouldRevalidate 拦截不必要的 Loader 调用:
import type { ShouldRevalidateFunctionArgs } from "react-router";
export function shouldRevalidate({
actionResult,
defaultShouldRevalidate,
formData,
}: ShouldRevalidateFunctionArgs) {
// 校验失败或错误响应时不触发重校验
if (actionResult?.error) {
return false;
}
// 针对特定 intent 跳过当前路由 loader 的刷新
const intent = formData?.get("intent");
if (intent === "track-progress") {
return false;
}
return defaultShouldRevalidate;
}
框架原生结合 AbortController。当新的导航或表单提交发生时,会自动中止上一轮未完成的数据请求,防止异步数据竞态覆盖。
基于 Web 标准 Cookie 实现服务端会话存储与强类型路由守卫。
参考文档:
import { createCookieSessionStorage } from "react-router";
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
path: "/",
sameSite: "lax",
secrets: [process.env.SESSION_SECRET || "default-secret-key"],
secure: process.env.NODE_ENV === "production",
},
});
export async function getSession(request: Request) {
return sessionStorage.getSession(request.headers.get("Cookie"));
}
export async function commitSession(session: any) {
return sessionStorage.commitSession(session);
}
export async function destroySession(session: any) {
return sessionStorage.destroySession(session);
}
在 loader 或 action 中执行身份校验,未通过时直接抛出 Response 或 data() 阻断渲染:
import type { Route } from "./+types/admin.users";
import { getSession } from "~/lib/session.server";
import { db } from "~/db";
import { usersTable } from "~/db/schema";
import { eq } from "drizzle-orm";
import { data, redirect } from "react-router";
export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request);
const userId = session.get("userId");
if (!userId) {
throw redirect("/login");
}
const currentUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, userId),
});
if (!currentUser || currentUser.role !== "admin") {
throw data("无权限访问管理后台", { status: 403 });
}
const allUsers = await db.select().from(usersTable);
return { users: allUsers };
}
React Router 提供路由级 ErrorBoundary 机制,在服务端渲染与客户端水合过程中捕获异常并实现局部故障隔离。
参考文档:
使用 isRouteErrorResponse 区分受控 HTTP 错误(如 401、403、404)与未捕获的运行时异常(500):
import { isRouteErrorResponse } from "react-router";
import type { Route } from "./+types/courses.$slug";
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (isRouteErrorResponse(error)) {
return (
<div className="error-container">
<h2>{error.status} {error.statusText}</h2>
<p>{error.data || "请求资源不存在或无访问权限"}</p>
</div>
);
}
return (
<div className="error-container">
<h2>应用运行异常</h2>
<p>{error instanceof Error ? error.message : "发生未预期的服务故障"}</p>
</div>
);
}
ErrorBoundary 捕获。