mcpField
R
react-router-v7

react-router-v7

github.com/mindrally开发30078 次安装4 周前更新
代码仓库

关于 react-router-v7

用于构建 React Router v7 路由模块的开发技能,面向 React 开发者,将界面、数据加载、变更和错误处理组织在路由边界,并强调类型化数据与渐进增强。

主要功能

  1. 01路由模块承载界面与数据
  2. 02Loader 和 action 模式
  3. 03路由级错误处理
  4. 04表单渐进增强
  5. 05独立测试路由逻辑

适用场景

  1. 01构建数据驱动的路由页面
  2. 02处理表单提交与验证
  3. 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。

构建路由模块的工作流

  1. 创建路由文件 — 在 app/routes/(或你配置的路由目录)下,为 URL 路径段添加文件;使用嵌套文件夹和文件来定义布局及动态路径段。
  2. 导出 loader — 在渲染前获取路由所需的数据;在 loader 开头验证路由参数和搜索参数。
  3. 导出组件 — 通过 useLoaderData() 读取 loader 数据并渲染 UI;让组件专注于展示。
  4. 导出 action — 处理来自 <Form> 或 useFetcher 的 POST/PUT/DELETE 提交;返回类型化数据(包括验证错误),而不是针对预期失败抛出异常。
  5. 导出 ErrorBoundary — 在本地处理抛出的错误(400、500 和意外异常),避免单个路由出错导致整个应用崩溃。
  6. 接入导航 — 使用 <Form>、useFetcher 和 useSubmit 进行修改,这样应用在禁用 JavaScript 时也能正常工作,在启用 JavaScript 时还能提供乐观 UI。
  7. 测试模块 — 分别为 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