发布时间:2026/9/6 20:13:14
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/9/6 20:13:14

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

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

2026/9/6 20:13:14

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

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

2026/9/6 21:08:18

A3报告培训课件设计:从问题解决逻辑到实操落地全指南

简介:这份PPT课件专注于A3报告制作培训,面向企业班组长、精益改善专员、质量管理人员及希望提升问题解决能力的职场人士。课件系统梳理了A3报告的定义与意义,强调以A3纸为载体实现简明沟通和深度分析,并围绕PDCA循环展开解决问题八…

2026/9/6 21:08:18

基于战略的绩效管理体系设计方案:从战略解码到落地闭环

简介:这份146页的全面绩效管理体系设计方案,以战略解码为主线,面向企业中高层管理者、HR及战略规划人员,重点解决绩效管理与战略重点脱节、指标分解与职责角色不匹配等常见难题。方案围绕战略篇、实施篇、机制篇、工具篇、名企篇展…

2026/9/6 21:08:18

快速跑起来 Qwerty Learner 打字练习完整指南

快速跑起来 Qwerty Learner 打字练习完整指南 【免费下载链接】qwerty-learner 为键盘工作者设计的单词记忆与英语肌肉记忆锻炼软件 / Words learning and English muscle memory training software designed for keyboard workers 项目地址: https://gitcode.com/GitHub_Tre…

2026/9/6 21:08:18

OpenClaw智能体框架:从安装部署到技能编排的实践指南

简介:OpenClaw是奥地利开发者发布的开源个人AI Agent框架,在2026年初凭借“本地优先、自主闭环执行”的特性迅速走红。这份129页的PDF指南正适合想系统学习OpenClaw的开发者、技术爱好者与AI产品从业者,帮助读者理清从入门到精通的完整路径。…

2026/9/6 21:08:18

大型企业全面绩效管理体系设计方案:从战略解码到落地实战

简介:这是一份146页的某大型企业全面绩效管理体系设计方案PPT,聚焦基于战略的绩效管理落地路径,适合企业高管、HR团队及绩效管理咨询人员参考,用于解决战略解码不清晰、考核指标与经营重点脱节等问题。整套资料共1个pptx文件&…

2026/9/6 21:03:17

Vector 日志管道 Kubernetes 部署与配置实战指南

Vector 日志管道 Kubernetes 部署与配置实战指南 【免费下载链接】vector A high-performance observability data pipeline. 项目地址: https://gitcode.com/GitHub_Trending/vect/vector 凌晨三点,一条线上故障把你叫起来,你 SSH 到十几台机器逐…

2026/9/6 0:06:59

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/6 0:06:59

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/6 0:06:59

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/6 0:06:59

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/6 0:06:59

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/6 0:06:59

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/6 11:40:10

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/6 19:33:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/6 10:19:40

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…