mcpField
R
remix-v2-data-flow

remix-v2-data-flow

github.com/existential-birds开发8541 次安装11 周前更新
代码仓库

关于 remix-v2-data-flow

面向 Remix v2 开发者的数据流参考技能,涵盖数据加载、变更、重新验证、流式响应和待处理状态。它通过决策指南与代码示例说明 API 选择及版本迁移注意事项。

主要功能

  1. 01Loader 与 action 模式
  2. 02类型化数据与响应处理
  3. 03使用 defer 流式传输及错误边界
  4. 04自动重新验证指引
  5. 05v1 到 v2 的待处理状态差异

适用场景

  1. 01选择 loader 或客户端副作用
  2. 02实现路由变更与重定向
  3. 03处理流式数据错误
  4. 04检查 GET 和 POST 表单的待处理界面

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/existential-birds/beagle --skill remix-v2-data-flow

安装后在 Claude Code 中输入/remix-v2-data-flow确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
actions.md
defer-await.md
loaders.md
revalidation.md

详细介绍

Remix v2 数据流

快速参考

Loader + 类型化读取:

import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export async function loader({ request }: LoaderFunctionArgs) {
  const invoices = await db.invoice.findMany();
  return json({ invoices });
}

export default function Invoices() {
  // typeof loader is a type ANNOTATION (not assertion) — drives SerializeFrom<T>.
  const { invoices } = useLoaderData<typeof loader>();
  return <InvoiceList invoices={invoices} />;
}

Action + 成功后重定向(PRG):

import { json, redirect, type ActionFunctionArgs } from "@remix-run/node";
import { useActionData, Form } from "@remix-run/react";

export async function action({ request }: ActionFunctionArgs) {
  const form = await request.formData();
  const parsed = NewProject.safeParse(Object.fromEntries(form));
  if (!parsed.success) return json({ errors: parsed.error.flatten().fieldErrors }, { status: 400 });
  const project = await db.project.create({ data: parsed.data });
  return redirect(`/projects/${project.id}`);
}

规范 API

路由模块导出 loader / action;组件通过 useLoaderData<typeof loader>() 和 useActionData<typeof action>() 读取结果。每次 action 执行后,Remix 都会自动重新验证页面上所有匹配路由的 loader,因此 UI 无需手动使缓存失效,也能与服务器保持一致。

签名:

  • loader: ({ request, params, context }: LoaderFunctionArgs) => Response | Promise<Response> — 仅在服务器端执行的读取操作,在 SSR 和通过 fetch 发起的客户端导航中运行。
  • action: ({ request, params, context }: ActionFunctionArgs) => Response | Promise<Response> — 仅在服务器端执行的非 GET 请求处理器(POST/PUT/PATCH/DELETE)。
  • json(data, init?: number | ResponseInit): TypedResponse<typeof data> — 便捷的 JSON Response 封装,可设置状态码和标头。
  • redirect(url, init?: number | ResponseInit): TypedResponse<never> — 30x 响应;默认状态码为 302。

导入:在 Node 上使用 @remix-run/node 中的服务器端工具(json、redirect、defer、类型参数);针对其他运行环境,分别替换为 @remix-run/cloudflare 或 @remix-run/deno。Hooks 和组件来自 @remix-run/react。

使用类型注解,而非类型断言

useLoaderData<typeof loader>() 是类型注解,不是 as 风格的类型断言。泛型会传入 SerializeFrom<typeof loader>,以模拟线上的数据转换:Date 会变成 string,Map/Set 会被压平,undefined 字段会被移除,类方法也会消失。如果你对 Date 字段调用 data.createdAt.getFullYear(),这就是运行时错误——类型早已表明它是 string。

v2 中何时可以不使用 json()

v2 并未改变底层约定:loader 和 action 必须返回 Response。json() 是一种便捷封装,会设置 application/json,并允许你指定状态码和标头。在 v2 中,直接返回对象也可行(Remix 会自动将其封装为 json()),但为了明确指定状态码和标头,并获得清晰的 TypedResponse<T> 类型,首选 json()。在以下情况应使用 json():

  • 需要非 200 状态码时(例如,验证错误使用 { status: 400 })。
  • 需要自定义标头时(缓存、Set-Cookie)。
  • 需要明确的 TypedResponse<T>,以便 useLoaderData<typeof loader>() 获得清晰的类型推断。

使用抛出实现短路

从 loader 或 action 中抛出 Response,会立即退出数据函数。可用于身份验证守卫(throw redirect("/login"))和 404 错误(throw new Response("Not Found", { status: 404 }) 或 throw json({ message }, { status: 404 }))。抛出普通 Error 不会被 useRouteError() / isRouteErrorResponse() 归类为路由响应。

流式传输中的拒绝:<Await errorElement> + useAsyncError()

通过 defer() 传递的 promise 被拒绝时,<Await errorElement={...}> 边界会在行内捕获错误;如果没有它,拒绝会向上冒泡至路由的 ErrorBoundary,导致整个页面被卸载,从而失去流式传输的优势。在 errorElement 内,调用 useAsyncError()(来自 @remix-run/react)读取拒绝值——这是流式传输场景下对应 useRouteError() 的方法。

function ReviewsError() {
  const error = useAsyncError(); // typed as `unknown`
  return <p>Failed to load reviews: {String(error)}</p>;
}

<Suspense fallback={<ReviewsSkeleton />}>
  <Await resolve={reviews} errorElement={<ReviewsError />}>
    {(r) => <ReviewList reviews={r} />}
  </Await>
</Suspense>

敏感数据

loader 返回的所有内容都会以 JSON 形式发送到浏览器。返回前应转换为安全的 DTO({ id, email, name });切勿返回完整的 Prisma User、密码哈希、API 密钥或内部标记。Loader 仅在服务器端执行,但其返回值会完整发送给客户端。

变更操作应放在 Action 中

每次 GET 导航都会运行 loader,而且预取可能会提前调用它;自动重新验证期间它也会再次运行。任何会变更持久化状态的操作都必须放在 action 中,并通过 <Form method="post"> 或 useFetcher 调用。直接在组件中调用 fetch() 请求 Remix 路由,会绕过重新验证、待处理状态和渐进增强;请改用 useFetcher().submit() / useFetcher().load()。

需要牢记的路由注意事项

  • 只会执行最深层匹配的操作。 嵌套在布局下的索引路由会发生冲突,除非使用 <Form action="/things?index" method="post" /> 指定目标路由。
  • useActionData 的作用域仅限当前路由。 它无法访问父级或子级路由的操作结果;若要共享结果,请提升操作所在路由,或使用带有 key 的 useFetcher。
  • 叶子路由的标头优先生效。 Remix 只会使用最深层匹配路由导出的 headers。除非显式合并 parentHeaders,否则会忽略父级缓存策略。
  • 默认情况下,执行操作后会重新验证页面上的所有路由——即使它们的参数没有变化。使用 shouldRevalidate 可跳过开销较大的父级加载器。

待处理状态(v1 → v2)

v2 中已移除 useTransition,请改用 useNavigation。在 v2 中,submission 对象会直接平铺到 navigation 上(fetcher 也一样;nav.formData/nav.formMethod 和 fetcher.formData/fetcher.formMethod 都是平铺的)。v2 中的 formMethod 现在是大写("POST",而不是 "post");像 nav.formMethod === "post" 这样的比较会静默地始终不匹配。fetcher.type 也已移除,应根据 fetcher.state 和 fetcher.formData 是否存在来分支处理。

GET 提交的状态流转为 idle → loading → idle。POST 的状态流转为 idle → submitting → loading → idle。仅在 "submitting" 状态下显示的加载指示器会漏掉 GET 表单。GET 提交在 loading 阶段仍会填充 nav.formData,且 nav.formMethod === "GET";因此,筛选表单的待处理 UI 应根据 formData 是否存在来分支处理,而不是检查 state === 'submitting'。对于 useFetcher,GET(<fetcher.Form method='get'> 和 fetcher.submit(..., {method:'get'}))和非 GET 请求都会进入 submitting;只有 fetcher.load() 会跳过 submitting。这与 useNavigation 相反,后者的 GET 请求会跳过 submitting。

判断关卡(决策顺序)

请按顺序回答。通过表示条件为真;选择同一行的 API 并停止。

loader 还是 useEffect
  1. 此路由的正确首次渲染是否需要这些数据(SSR、预取、操作后的自动重新验证)?
    • 通过 → loader + useLoaderData<typeof loader>()。停止。
    • 未通过 → 第 2 步。
  2. 获取数据是否由挂载后的用户交互、计时器或订阅驱动(而非进入路由)?
    • 通过 → useEffect / 事件处理程序。停止。
    • 未通过 → 优先使用 loader 和重新验证;不要在 effect 中复制导航逻辑。
json()、原始 Response 还是 defer()
  1. 是否需要在页面快速渲染的同时流式传输某些返回字段(慢速查询、开销较大的聚合)?
    • 通过 → defer({ critical: await…, slow: promiseWithoutAwait }) + <Suspense><Await>…</Await></Suspense>。停止。
    • 未通过 → 第 2 步。
  2. 是否需要自定义状态码、自定义标头或显式的 TypedResponse<T> 类型?
    • 通过 → json(data, init)(对于 3xx,使用 redirect(url, init))。停止。
    • 未通过 → 第 3 步。
  3. 是否需要非 JSON 请求体(二进制、纯文本、流式文件)?
    • 通过 → 构造原始的 new Response(body, init)。停止。
    • 未通过 → 默认使用 json(data)——这是 v2 对象载荷的文档规定契约。
<Form> / 路由操作还是 useFetcher
  1. URL 或历史记录栈是否应发生变化(书签 / 分享 / 返回时回到先前的屏幕)?
    • 通过 → 使用 <Form method="post">,提交到路由的 action。停止。
    • 未通过 → 第 2 步。
  2. 变更是否留在同一条路由上(行内编辑、列表行切换、弹出框、乐观 UI)?
    • 通过 → useFetcher() / fetcher.Form / fetcher.submit()。停止。

附加文档

  • 加载器:请参阅 references/loaders.md,了解 loader 签名、类型化的 useLoaderData、json() 与原始 Response 的区别、redirect()、抛出错误、敏感数据筛选以及参数处理。
  • 操作:请参阅 references/actions.md,了解 action 签名、FormData 解析、useActionData<typeof action>()、zod/valibot 验证以及成功后重定向。
  • 延迟加载与等待:请参阅 references/defer-await.md,了解 defer() + <Await> + <Suspense>、流式传输有助于 TTFB 的场景以及错误处理。
  • 重新验证与待处理状态:请参阅 references/revalidation.md,了解自动重新验证、shouldRevalidate、useRevalidator、useNavigation(以及 v1 中 useTransition 的重命名)。

v1 → v2 快速对比

关注点v1v2
导航钩子useTransition()useNavigation()
提交数据结构transition.submission.formMethod扁平的 nav.formMethod / nav.formData
formMethod 大小写"post""POST"(大写)
Fetcher 类型字段fetcher.type === "actionSubmission"根据 fetcher.state + fetcher.formData 分支
Loader 参数类型LoaderArgs / ActionArgsLoaderFunctionArgs / ActionFunctionArgs
返回数据必须使用 json(data)json(data) 仍是文档规定的约定

评论

支持 Markdown