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

发布时间:2026/11/5 7:42:15

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),仅供参考
延伸阅读

更多相关文章

2026/11/3 18:33:27

通达信九转趋势主图指标:源码解析与实战应用

简介:这是一份通达信九转趋势主图指标源码文档,适合使用通达信进行技术分析、想自制主图指标的交易者学习参考。资源仅包含一个doc文件,压缩包74KB,文档内给出了完整指标公式源码,覆盖九转序列自动判断、趋势支撑与压力…

2026/10/31 1:16:44

同步发电机并网建模与动态仿真:从并网条件到参数整定全解析

简介:围绕发电机并网模型的建立与并网过程仿真,这份PDF文档面向电力系统自动化、电气工程等专业的学生与工程技术人员,适用于课程设计、毕业设计、并网操作研究,也可供互联网能源电力类项目参考。文档从并网条件入手,分…

2026/11/5 7:38:10

ADS122U04+STM32L476RG:工业与HVAC精密温度采集系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 7:38:10

ADS122U04搭配STM32F407ZG:高精度多通道温度采集方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 7:38:10

工业测温实战:ADS122U04与PIC18F85J50的多路温度采集方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 7:38:10

ADS122U04+STM32F765ZI工业温度采集模块:PT100/热电偶精确测量方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 7:38:10

工业级温度测量系统设计:ADS122U04IPWR与STM32F101ZG协同方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 7:33:10

Univer开源在线表格实战:从Canvas渲染到私有化部署的完整指南

第一次注意到 Univer,是在一个很现实的场景里:我们要给内部 ERP 系统加一个数据预览模块,老板要求“像 Excel 一样、但必须在浏览器里用”,最好还能直接嵌进现有页面。一开始我们想过直接用表格组件硬渲染,但拖到十万行…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 0:02:07

多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中…

2026/11/5 0:02:07

大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#…

2026/11/5 0:02:07

鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑