关于 react-router-v7
用于构建 React Router v7 路由模块的开发技能,面向 React 开发者,将界面、数据加载、变更和错误处理组织在路由边界,并强调类型化数据与渐进增强。
主要功能
- 01路由模块承载界面与数据
- 02Loader 和 action 模式
- 03路由级错误处理
- 04表单渐进增强
- 05独立测试路由逻辑
适用场景
- 01构建数据驱动的路由页面
- 02处理表单提交与验证
- 03将错误限制在单个路由内
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill react-router-v7安装后在 Claude Code 中输入/react-router-v7确认已加载。
详细介绍
React Router v7
React Router v7 框架模式将路由、数据加载和修改统一到路由模块中。每个模块对应一个 URL 路径段,并导出 loader、action、default 组件和 ErrorBoundary。
构建路由模块的工作流
- 创建路由文件 — 在
app/routes/(或你配置的路由目录)下,为 URL 路径段添加文件;使用嵌套文件夹和文件来定义布局及动态路径段。 - 导出 loader — 在渲染前获取路由所需的数据;在 loader 开头验证路由参数和搜索参数。
- 导出组件 — 通过
useLoaderData()读取 loader 数据并渲染 UI;让组件专注于展示。 - 导出 action — 处理来自
<Form>或useFetcher的POST/PUT/DELETE提交;返回类型化数据(包括验证错误),而不是针对预期失败抛出异常。 - 导出 ErrorBoundary — 在本地处理抛出的错误(400、500 和意外异常),避免单个路由出错导致整个应用崩溃。
- 接入导航 — 使用
<Form>、useFetcher和useSubmit进行修改,这样应用在禁用 JavaScript 时也能正常工作,在启用 JavaScript 时还能提供乐观 UI。 - 测试模块 — 分别为 loader、action、验证失败和错误边界编写测试,不依赖渲染后的组件树。
路由模块
- 使用路由模块作为路由 UI、loader 数据、action、元数据和错误边界的边界——路由模块是代码拆分和数据加载的基本单元。
- 保持路由模块小巧;将共享 UI 移至
app/components/,将可复用的数据访问逻辑移至服务层(例如app/services/posts.server.ts)。 - 当项目已配置为使用基于文件的路由时,在框架模式下优先采用这种方式;对于高级或动态路由树,则使用显式的
routes.ts配置。 - 使用嵌套路由共享布局并逐步展示内容——父路由的
<Outlet />会渲染匹配的子路由。 - 为可恢复的路由失败导出路由专属的
ErrorBoundary组件,而不要依赖单个全局应用边界。
// app/routes/posts.$postId.tsx
import { Form, useLoaderData, useNavigation } from "react-router";
import type { Route } from "./+types/posts.$postId";
import { getPost, updatePost } from "~/services/posts.server";
export async function loader({ params }: Route.LoaderArgs) {
const post = await getPost(params.postId);
if (!post) {
throw new Response("Not Found", { status: 404 });
}
return { post };
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
const title = String(formData.get("title") ?? "");
if (title.trim().length === 0) {
return { ok: false as const, errors: { title: "Title is required" } };
}
await updatePost(params.postId, { title });
return { ok: true as const, errors: null };
}
export default function PostDetail() {
const { post } = useLoaderData<typeof loader>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<input name="title" defaultValue={post.title} />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</button>
</Form>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (error instanceof Response && error.status === 404) {
return <p>Post not found.</p>;
}
return <p>Something went wrong loading this post.</p>;
}
数据加载
- 使用 loader 加载应在渲染前可用的路由数据——这样页面不会为其运行所必需的数据显示加载指示器。
- 让 loader 保持确定性且无副作用;loader 应读取数据,而不是写入数据。
- 在 loader 边界验证参数和搜索参数(例如解析并检查
page查询参数的范围),而不是在下游直接信任这些参数。 - 返回类型化数据,并通过
useLoaderData<typeof loader>()使用,而不是在组件中重复获取数据的逻辑。 - 只有在可显著提升感知性能时才使用延迟或流式处理模式(
defer或为<Await>返回 Promise),不要默认使用。
修改
- 使用 action 处理路由修改和表单提交;当对该路由进行
POST/PUT/PATCH/DELETE导航或提交 fetcher 时,action 会运行。 - 优先使用
<Form>、useFetcher和useSubmit实现渐进增强——即使没有客户端 JavaScript,它们也能正常工作;有 JavaScript 时,还能叠加乐观 UI。 - 修改后重新验证受影响的 loader 数据;action 完成后,React Router 会自动重新验证当前路由树中的 loader,因此应避免在
useEffect中手动重新获取数据。 - 将验证错误作为类型化的 action 数据处理(
return { errors }),而不是抛出通用异常——仅在实际发生 HTTP 状态错误(404、401、500)时才抛出Response。 - 不要在客户端 action 中放置仅限服务端使用的机密和特权操作(API 密钥、直接写入数据库);应在
.server.ts模块中执行这些操作,这类模块会从客户端包中剔除。
// Optimistic UI with useFetcher
function LikeButton({ postId, liked }: { postId: string; liked: boolean }) {
const fetcher = useFetcher();
const optimisticLiked = fetcher.formData
? fetcher.formData.get("liked") === "true"
: liked;
return (
<fetcher.Form method="post" action={`/posts/${postId}/like`}>
<input type="hidden" name="liked" value={String(!optimisticLiked)} />
<button type="submit">{optimisticLiked ? "Unlike" : "Like"}</button>
</fetcher.Form>
);
}
导航和状态
- 将可共享的状态存储在 URL 参数或搜索参数中(筛选条件、分页、选中的标签页),这样链接和重新加载页面都能还原相同的视图。
- 使用
useState将临时 UI 状态(下拉菜单的展开/收起、悬停状态)保存在组件本地。 - 使用待处理的导航状态(
useNavigation()、fetcher.state)显示乐观 UI 或加载 UI,而不是单独维护全局加载指示器状态。 - 避免使用全局状态(Redux/Zustand/Context)存储属于路由加载器的数据——这会重复路由器自身的缓存,并导致数据过时。
TypeScript 和测试
- 为加载器和操作的返回值指定类型;使用生成的
Route.LoaderArgs/Route.ActionArgs类型(来自./+types/<route>),以完整推断参数和返回值的类型。 - 为路由加载器、操作、验证失败和错误边界添加测试,将其作为独立单元进行测试,而不依赖完整的组件渲染。
- 对关键表单和导航流程使用集成测试(例如 Playwright、配合路由器测试工具的 Testing Library)。
- 在路由服务边界(
app/services/*.server.ts)模拟网络和持久化,而不是在组件深处模拟fetch。
常见错误
- 在
useEffect中重复获取加载器数据——这会造成加载瀑布流,违背路由级数据加载的初衷。 - 直接在加载器中修改数据——加载器可能会运行多次(重新验证、预取),因此必须保持只读。
- 将路由错误隐藏在单个通用的应用级兜底
ErrorBoundary后面,而不是使用保留导航和布局的路由级边界。 - 只在组件中进行身份验证检查(例如从
useEffect重定向),但底层加载器数据受保护——应在加载器本身中检查并重定向,确保绝不会获取或短暂显示任何受保护的数据。
评论
支持 Markdown