恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

React Router 的 useLoaderData / useActionData 类型推断 ADR:从盲目类型断言到基于泛式的端到端类型安全

  • 首页
  • 资讯中心
  • /
  • React Router 的 useLoaderData / useActionData 类型推断 ADR:从盲目类型断言到基于泛式的端到端类型安全

相关资讯

React Router 架构决策 ADR-0008:TypeScript 模板为何只转换 app 代码为 JavaScript 2026/9/6 20:13:17
通达信九转趋势主图指标:源码解析与实战应用 2026/9/6 20:13:17
同步发电机并网建模与动态仿真:从并网条件到参数整定全解析 2026/9/6 20:13:17

最新资讯

智慧小区弱电系统规划设计方案:子系统选型与集成平台落地指南
FastChat 与 Vicuna 权重版本指南:兼容性对照、Prompt 模板原理与 Delta 权重合并实操
JAX 常见错误详解:jax.errors 错误类型、触发场景与修复方法全指南
1Panel 官方文档(老挝语版)精读:开源 Linux 服务器管理面板的部署与 1pctl CLI 实战
Win11连共享打印机失败?0x0000011b、0x80070035、0x000006d9排查流程
施乐C3055维修手册PDF实战:从故障定位到排除闭环

今日推荐

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

React Router 的 useLoaderData / useActionData 类型推断 ADR:从盲目类型断言到基于泛式的端到端类型安全

发布时间:2026/9/6 20:13:17
React Router 的 useLoaderData / useActionData 类型推断 ADR:从盲目类型断言到基于泛式的端到端类型安全 React Router 的 useLoaderData / useActionData 类型推断 ADR从盲目类型断言到基于泛式的端到端类型安全【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router本文解析 React Router 架构决策记录 0003 的核心内容为什么 Remix v1.6.4 时代的useLoaderDataMyData()泛式本质上是一次盲目断言、Date序列化陷阱如何暴露该设计的缺陷以及显式提供隐式输入类型、再推断返回类型这一决策如何解决 loader/action 与组件之间跨网络的类型对齐问题。读完本文你将理解该 ADR 的完整论证链、六条解决标准以及这套设计在 React Router 7 源码中SerializeFrom、data()等的最终落地形态还能看清它后来如何被 ADR 0012 类型推断 的 typegen 方案取代。一、ADR 背景v1.6.4 的手工对类型时代该 ADR日期 2022-07-11的目标是以优秀的开发者体验DX实现useLoaderData和useActionData的端到端类型安全。在 Remix v1.6.4 中两个 hook 的泛式都要求用户手动指定一个数据类型type MyLoaderData { /* ... */ }; type MyActionData { /* ... */ }; export default function Route() { const loaderData useLoaderDataMyLoaderData(); const actionData useActionDataMyActionData(); return div{/* ... */}/div; }为了获得端到端的类型安全用户还必须在loader/action中保证json泛式使用同一个类型export const loader: LoaderFunction () { return jsonMyLoaderData({ /* ... */ }); }; export const action: ActionFunction () { return jsonMyActionData({ /* ... */ }); };也就是说一份数据形状要写两遍类型甚至三遍且没有任何机制保证两边一致。二、深挖 v1.6.4 源码泛式只是把any强转成TADR 追溯了 v1.6.4 中remix-run/react的源码发现useLoaderData返回的实际上是一个any类型被隐式强转成泛式传入的任何类型export function useLoaderDataT AppData(): T { return useRemixRouteContext().data; } interface RemixRouteContextType { data: AppData; // AppData any id: string; } export type AppData any;化简之后就是let data: any; // 某处loader 被调用并把某个值赋给 data function useLoaderDataT(): T { return data; // -- TypeScript 把这个 any 强转为 T }关键结论useLoaderData的返回类型既不基于data是怎么被设置的即loader的返回值也不做任何数据校验而是盲目地把data强转为用户传入的泛式T。双重代价冗余代码 序列化陷阱ADR 指出了当前方案的两个问题DX 差、代码冗余用户必须手写数据类型的重复声明。数据形状一旦变化既要改声明的type/interface又要改json的实参——而这些类型本可以从json的实参中推断出来。Date序列化陷阱footgun当前方案鼓励用户给json和useLoaderData传同一个类型但这恰恰是个坑——json可以接受Date这类可 JSON 序列化的类型而useLoaderData拿到的却是序列化后的类型type MyLoaderData { birthday: Date; }; export const loader: LoaderFunction () { return jsonMyLoaderData({ birthday: new Date(February 15, 1992) }); }; export default function Route() { const { birthday } useLoaderDataMyLoaderData(); // ^ useLoaderData 骗过 TypeScript 认为这是 Date实际上运行时它是一个 string }useActionData同理。数据经过网络传输必然是 JSON 序列化后的产物而类型系统对此视而不见——这是一整类编译通过、运行出错的隐患。三、解决方案标准Solution CriteriaADR 给出了六条硬约束任何候选方案都必须满足useLoaderData/useActionData的返回类型应当从loader/action推断出来而不是盲目类型断言loader/action自身的返回类型应当是可推断的这就要求json的返回类型能从其实参推断不允许模块副作用因此像makeLoader这样的高阶函数方案被直接排除;json应当允许JSON.stringify允许的一切;json应当只允许JSON.stringify允许的东西useLoaderData不应返回JSON.parse无法产生的任何东西。第 4、5、6 条共同刻画了核心不变量loader 端的可序列化输入约束 与 组件端的反序列化输出类型约束必须严格对应从而消灭Date陷阱这一类错误。四、关键洞察loader是useLoaderData的隐式输入对用 TypeScript 泛式推断 hook 返回类型曾有过犹豫ADR 引用了社区讨论因为TypeScript 泛式天生适合描述/推断输入而不是用来盲目断言输出。突破点在于认识到loader和action其实是useLoaderData/useActionData的隐式输入。换句话说如果保证loader和useLoaderData运行在同一进程中不跨网络我们完全可以写成useLoaderData(loader)把loader变成显式输入// 概念上 loader 是 useLoaderData 的输入 function useLoaderDataLoader extends LoaderFunction(loader: Loader) { /*...*/ }现实中loader在浏览器运行时并不存在它跑在服务端useLoaderData需要在编译期获知loader的类型。而loader与useLoaderData由框架统一管理、跨越网络协作拿到的数据与自己的loader不对应是极其罕见的边界情况——因此用一个泛式参数把loader的类型显式注入给 hook 是安全且合理的。ADR 还类比为 Prisma尽管存在编译期之后、运行期之前数据库 schema 被修改这类罕见边界情况Prisma 依然从运行期可用的 schema 推断类型。五、决策显式提供隐式输入的类型再推断返回值最终决策为useLoaderData显式提供其隐式输入loader的类型然后由 hook 推断自己的返回类型action/useActionData同理export const loader async (args: LoaderArgs) { // ... return json(/*...*/); }; export default function Route() { const data useLoaderDatatypeof loader(); // ... }注意这里不再是手写MyLoaderData这类独立类型而是typeof loader——类型直接锚定在loader的真实返回类型上冗余声明被彻底消除。同时useLoaderData推断出的返回类型只包含可序列化的JSON类型从类型层面兑现了只返回JSON.parse能产生的东西这条标准。省略泛式时返回unknown如果useLoaderData/useActionData省略泛式就返回any会掩盖潜在的类型错误。ADR 决定改为返回unknowntype MyLoaderData { /*...*/ }; export default function Route() { const data useLoaderData(); // ^? unknown }ADR 同时注明由于这是破坏性变更把缺省返回类型改为unknown被排期到 v2。弃用非推断的泛式写法直接传一个手写的非推断类型给useLoaderData本质是在隐藏一次不安全的类型断言。ADR 决定弃用该写法引导用户改用显式类型断言——断言清楚地表达了我在此处做了假设export default function Route() { const dataGeneric useLoaderDataMyLoaderData(); // -- 将被弃用 const dataCast useLoaderData() as MyLoaderData; // - 改用这种写法 }六、决策的后果与硬性约束ADR 明确列出了该决策带来的行为变化用户仍可继续提供非推断类型方式是对useLoaderData/useActionData的返回值做类型断言用户通过在泛式中写typeof loader/typeof action来选择性加入类型推断loader/action的返回类型成为useLoaderData/useActionData推断类型的唯一事实来源source of truth用户不再需要为跨网络对齐类型而写冗余代码;useLoaderData/useActionData的返回类型将与json调用中数据序列化后的类型严格对应消灭一整类错误;选择类型推断时不应再标注LoaderFunction/ActionFunction——它们会覆盖推断出的更窄的返回类型1。 最关键的硬性约束选择类型推断的用户必须从json返回TypedResponse绝不能返回裸对象const loader () { // NO return { hello: world }; // YES return json({ hello: world }); };只有经过json后在 React Router 7 中更名/演进为data包装的数据其返回类型才能被正确推断并施加序列化约束裸返回的对象类型无法参与这一推断链条。七、当前仓库中的落地印证SerializeFrom与data()ADR 描述的是历史设计但 React Router 7 的源码完整保留并工程化了这套思想。可以对照以下实现逐条验证1. hook 的当前签名——泛式输入 序列化后输出。在 packages/react-router/lib/hooks.tsx 中export function useLoaderDataT any(): SerializeFromT { let state useDataRouterState(DataRouterStateHook.UseLoaderData); let routeId useCurrentRouteId(DataRouterStateHook.UseLoaderData); return state.loaderData[routeId] as SerializeFromT; } export function useActionDataT any(): SerializeFromT | undefined { let state useDataRouterState(DataRouterStateHook.UseActionData); let routeId useCurrentRouteId(DataRouterStateHook.UseLoaderData); return (state.actionData ? state.actionData[routeId] : undefined) as | SerializeFromT | undefined; }返回值不再是裸的T而是SerializeFromT——这正是 ADR 推断出的返回类型只包含可序列化 JSON 类型 的类型学实现。官方文档 useLoaderData 与 useActionData 中的示例仍然使用useLoaderDatatypeof loader()这一 ADR 确立的用法。2.SerializeFrom的完整定义。在 packages/react-router/lib/types/route-data.ts 中该类型先判断函数参数的形态再决定走服务端数据还是客户端数据路径export type SerializeFromT T extends (...args: infer Args) unknown ? Args extends [ | ClientLoaderFunctionArgs | ClientActionFunctionArgs | ClientDataFunctionArgsunknown, ] ? ClientDataFromT // 客户端函数数据不过网络原样保留 : ServerDataFromT // 服务端函数施加序列化转换 : T;ServerDataFrom会对其返回值套用SerializeT递归映射同文件的 L16-L46先识别unstable_SerializesTo品牌类型已可序列化的类型原样保留函数一律映射为undefined并递归处理Promise、Map/Set、数组、元组与对象——这比 ADR 原始设想的纯 JSON 字符串化更进一步与 turbo-stream 传输层支持的容器类型精确对应;ClientDataFrom则跳过序列化映射因为clientLoader/clientAction的数据不跨网络同文件还定义了GetLoaderData/GetActionDataL174-L208处理loaderclientLoaderclientLoader.hydrateHydrateFallback组合下的数据形态——这恰是后续 ADR 0012 中那张组合表格的类型学基础。3. 文件内的类型级测试。route-data.ts 末尾内置了一组ExpectEqual...类型测试直接验证了 ADR 关心的行为例如Expect Equal ServerDataFrom() { a: string; b: Date; c: () boolean; d: unstable_SerializesTonumber }, { a: string; b: Date; c: undefined; d: number } c函数被映射为undefined、d带序列化品牌被映射为number——正是loader 端只允许可序列化输入、组件端只得到可反序列化输出这一不变量的可执行证明。4.json约束的运行时对应物。ADR 中必须走json的约束在当前仓库对应data()辅助函数及其Serializable入参约束定义于 packages/react-router/lib/server-runtime/single-fetch.tsSerializable是一个递归类型string | number | boolean | bigint | Date | URL | RegExp | Error | Map | Set | Promise | 数组 | 对象的递归联合data(value: Serializable, init?)的签名把它变成了编译期检查。这同时满足了 ADR 解决标准中json允许且只允许JSON.stringify允许的东西两条——函数、Symbol等不可序列化值在类型层面即被拒绝。八、结局被 ADR 0012 取代——从typeof loader到 typegenADR 头部明确标注了状态Superseded by #0012即 decisions/0012-type-inference.md日期 2024-09-20。0012 指出typeof loader方案虽有实质改进区别于useParamsid那种纯断言泛式但仍是样板代码且随应用规模放大容易出错尤其clientLoaderhydrateHydrateFallback的组合下泛式的正确写法极其繁琐。最终方案是放弃用户手写泛式改为代码生成typegen对routes.ts返回的每一条路由把 route 模块的类型生成到 gitignored 的.react-router/types目录下路径镜像如app/routes/product.tsx对应types.product.ts借助tsconfig.json的rootDirs选项让用户像从兄弟文件一样import { LoaderArgs, DefaultProps } from ./types.product并把params、loaderData、actionData作为 props 直接注入default组件——useLoaderData等 hook 因向后兼容保留但目标是逐步弃用。0012 还系统否决了defineRoute、defineLoader系列、Svelte Kit 式零成本类型安全语言服务插件注入和 TypeScript 插件等替代路线理由包括 tree-shaking/HMR 兼容性与工具链typescript-eslint、tsc的互操作。从 0003 到 0012 的演进脉络值得注意0003 确立了以 loader/action 返回类型为类型事实来源 序列化感知这两个核心原则0012 只是把由用户手写typeof loader泛式替换为由 typegen 自动注入而 0003 中SerializeFrom所依赖的序列化映射逻辑则原样保留在今天的 route-data.ts 中。九、实践要点小结在本仓库对应的 React Router 7 代码中推荐写法仍是useLoaderDatatypeof loader()见 hooks.tsx 的官方示例注释若框架模式已启用 typegen则优先使用生成的Route.LoaderArgs/ props 方案;loader/action 必须返回data(...)v7 中json的继任者包装的TypedResponse裸对象返回会绕过序列化感知类型需要给特殊自定义序列化类型声明序列化后形态时使用 unstable_SerializesTo 品牌类型而不是手写断言;理解 ADR 的论证结构现状剖析 → 解决标准 → 关键洞察 → 决策 → 后果与约束是阅读本仓库decisions/目录下其他 ADR 的通用模板ADR 模板 可作参考。原 ADR 脚注引用了当时 TypeScript 提案中的satisfies运算符它能约束函数类型的同时保留更窄的推断返回类型从而让LoaderFunction/ActionFunction与类型推断共存。↩【免费下载链接】react-routerDeclarative routing for React项目地址: https://gitcode.com/GitHub_Trending/re/react-router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号