mcpField
R
reatom-review

reatom-review

github.com/reatom测试与质量1.4k82 次安装1 天前更新
代码仓库

关于 reatom-review

reatom-review 是用于按 Reatom v1001 规范审查 Agent 工作的技能,面向代码审查与验证。它强调依据权威参考资料核对行为、API、测试和状态管理模式,并优先报告具体问题。

主要功能

  1. 01按 Reatom v1001 规范审查
  2. 02核查异步与上下文传播
  3. 03检查状态建模和命名
  4. 04审视副作用与订阅生命周期
  5. 05基于参考资料提出具体修正

适用场景

  1. 01检查 Agent 提交的 Reatom 代码
  2. 02发现过时 API 和误导性文档
  3. 03评估测试是否覆盖关键行为

安装

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

$npx skills add https://github.com/reatom/reatom --skill reatom-review

安装后在 Claude Code 中输入/reatom-review确认已加载。

详细介绍

Reatom 代码审查

使用此技能,根据 Reatom v1001 的实践验证 Agent 的工作。保持审慎:目标是找出错误行为、误导性文档、过时的 API 用法、薄弱的测试,以及看似合理但实际有问题的模式。

参考资料

在验证 API 用法、扩展选项或文档声明之前,还要加载 reatom 技能,并阅读 REFERENCE.md 中相关章节。该文件是 v1001 API 的权威参考。实现(而非审查)时,请直接使用 reatom 技能。

审查原则

  • 先列出问题。检查正确性之前不要称赞。
  • 批评被审查的工作,而不是作者。
  • 与通用前端惯例冲突时,以 REFERENCE.md 作为本地事实来源。
  • 引用具体文件和符号,并说明失效方式。
  • 提出一个明确的修复方案,避免含糊的建议。
  • 如果某项更改只有在特殊理由下才可接受,请询问原因或将其标记为风险。
  • 不要仅因为 TypeScript 编译通过就认可 Reatom 代码;还要审查上下文传播、取消、惰性、命名和订阅。

必查项

  1. 异步读取和查询:

    • 对幂等的读取/查询数据,应使用 computed(async () => ...).extend(withAsyncData(...))。
    • 指出挂载时触发的请求、effect 请求、refs、组件本地异步状态或命令式加载器,除非代码有明确的非查询用途。
    • 对变更/命令,应使用 action(async () => ...).extend(withAsync(...)),并在需要时搭配 withAbort 或事务。
    • 只有在 withAsync/withAsyncData 启用 { status: true } 时,才可使用 .status()。否则优先使用 .ready()、.pending() 和 .error()。
    • 在 action 上使用 .retry() 需要 withAsync({ cacheParams: true });否则调用时 retry 会抛出异常。Computed 无需选项即可重试。
    • 扩展顺序很重要:必须先应用 withAsync/withAsyncData,再应用 withCache(在 withCache 之后附加 withAsync 会抛出异常)。检查每个 .extend(...) 链的顺序,而不仅是确认扩展存在。
    • 异步辅助 atom 是 getter:使用 .data()、.ready()、.error() 和 submit.error()。不要将 atom 对象本身作为静态值渲染或解构。
    • 状态标志是属性,例如 status.isPending,而不是函数。使用目标对象的 .error() atom;不要臆造 status.error。
  2. wrap 和异步上下文:

    • 在离开 Reatom 框架的异步边界(fetch、计时器、DOM Promise 等)处使用 await wrap(promise)。经过包装的 await 之后,后续代码会回到上下文中,可直接调用 atoms/actions。
    • 仅当 fn 会传给外部调用方(DOM 监听器、计时器、第三方回调)时,才使用 wrap(fn)。它会返回一个经过包装的函数,不会调用 fn。如果 wrap(() => atom.set(...)) 没有赋值,也没有传递给外部回调,请指出问题。
    • 指出在 actions、effects 或异步 computeds 中无意义的 wrap(() => atom.set(...))();立即执行经过包装的 IIFE 没有作用,直接在 Reatom 框架中调用 atom.set(...) 即可。
    • 指出 await wrap(fetch(url)).then(...);应改为包装整个 Promise 链,或分别包装每个 await 步骤。
    • 如果 .then(...) 回调、DOM 回调、计时器、requestAnimationFrame 或外部事件监听器在没有 wrap 或 onEvent 的情况下调用 Reatom 状态,请指出问题。
    • 每次 await 之后都要检查:如果后续代码会在异步续体中调用 atoms/actions,被 await 的 Promise 通常应经过包装。
    • 等待 DOM 或外部事件时,优先使用 await wrap(onEvent(...)),而不是原生事件监听器。
    • wrap() 应用于了解 Reatom 的 actions、effects、computeds 或回调中,而不应放在普通的可复用 API 辅助函数中。
    • 降级后的 async/await 可能会破坏上下文传播。如果出现严格上下文错误,请指出将异步代码转换为 .then() 链的构建/测试目标。
    • 不要要求包装传给 Reatom hook(例如 withCallHook)的回调;这些 hook 已在 Reatom 上下文中运行。
  3. 状态建模:

    • 写入应通过 .set(...) 进行。带参数调用响应式 atom(counter(5))会抛出异常;带参数调用 computed 也会抛出异常。标记所有通过位置参数调用进行写入的情况;读取时应使用无参数调用。
    • Action 状态(getCalls、action() 返回的列表)是临时的——会在下一个清理队列刻中清除。标记将 action 的调用列表存储为持久状态或之后读取该列表的代码;应将载荷持久化到 atom 中。
    • 集合原语(reatomMap、reatomSet、reatomArray、reatomRecord、reatomLinkedList)应通过其 action/不可变方法更新。标记对其状态进行原地修改的代码(map().set(...)、array().push(...)):这会跳过失效处理并破坏相等性检查。
    • .extend(...) 不能替换 atom 引用,也不能覆盖现有键——方法名冲突会在运行时抛出异常。标记分配的键会遮蔽 set、subscribe、extend 或更早扩展方法的情况。
    • 动态对象中的可变字段应 atom 化。
    • 如果使用条目本地 atom 会更清晰,则标记将 UI 状态规范化并平行存储的做法,例如分别存储 selectedIds、checkedIds 或编辑映射。
    • Action 与纯转换:
      • 只将一种数据形状映射为另一种数据形状的函数——不进行 IO、不调用 atom.set、不调用其他 action——不是 action。应使用普通函数或 computed。
      • 执行副作用(网络、存储、计时器、DOM、日志记录)或更改 Reatom 状态(atom.set、调用 action)的函数是 action,应为其命名。
    • 标记薄 computed 包装器:computed(() => helper(model)) 中的 helper 只读取模型上的 atom getter 并返回派生值。这类 helper 只是重复实现了 computed,且无法在 Reatom 之外复用。应将派生逻辑放在 computed 主体中,或通过 withComputed/.extend 附加到父级;不要将其拆分为普通 helper 和透传 computed。
    • 标记接收含 atom 的模型、并通过 .data()/atom getter 进行响应式派生的普通 helper。它们会隐藏响应式图,导致薄 computed 包装器,并且在 computed/action 上下文之外调用时不安全。
    • 对本地/简单更新,直接使用 atom.set 没问题。标记只负责转发值给 atom 的“透传” action。
    • 带有副作用和多步骤流程的复杂转换应实现为有名称的 action。
    • 相关状态和 action 应通过 .extend(...) 或 withActions(...) 归组到其父级,而不是作为并列导出分散定义。
    • 遵循 reatomBoolean 和 reatomRoute 等核心模式:先创建父 atom,再通过 .extend 附加相关方法、子 computed、加载器、路由工厂和辅助函数。
    • 对于有作用域的状态,可考虑 computed 工厂模式:由 computed 读取作用域键,并返回该作用域的 atom/action/表单,因此键变化时,内部图也会随之替换。
    • computed 工厂是通用模式,并非只适用于路由。应在选中实体、编辑会话、模态框、标签页,以及任何具有独立状态生命周期的命名单元工作附近留意这类工厂。
    • 应有意选择工厂契约:当非活动作用域属于普通状态时返回 null;当在命名作用域之外读取属于错误时则抛出异常。
  4. 命名与可追踪性:

    • Atom、计算值、effect 和 action 都应命名。
    • Atom、action 和 model 工厂必须使用 reatom* 前缀(例如 reatomFolderTreeNodeUi、reatomGalleryImage、reatomUser),而不是 get*、create* 或其他通用动词。标记那些会分配具名 atom、计算值、effect 或 action,却看起来像普通 getter 或构造函数的工厂函数。
    • 嵌套/动态名称应保留结构,例如 users.page、users#${id}.name 或 ${target.name}.ready。
    • 对热路径或噪声较多的相对状态和 action,在其跟踪名称的相应片段前加 _:与指针移动、滚动、调整大小或动画帧相关的 atom、计算值、effect 和 action。例如 lightbox._panMove、lightbox._controlsActivity、lightbox._hideControlsAfterInactivity、imageGrid._width。这样可以让日志更易读。
    • 标记共享 model 和示例中的匿名原语。
  5. 副作用、钩子和订阅:

    • 使用 computed 派生状态,使用 effect 执行副作用。
    • computed 是惰性的;请确认预期会加载的数据有订阅者,或存在明确的路由/渲染路径。
    • 未订阅的 computed 会在每次读取时重新验证;已订阅的则会通过推送缓存。标记在热循环中读取开销较大的未订阅 computed,以及热路径上没有订阅者的开销较大的 computed——可考虑使用 withMemo 或添加订阅。
    • 订阅者回调和队列副作用会异步刷新(微任务)。标记这类代码(尤其是测试):在 .set(...) 后立即断言订阅者已同步触发;应等待一个微任务/sleep(0),或直接读取 atom。
    • 使用默认全局上下文的测试必须通过 context.reset()(或 context.start 作用域)隔离状态。标记 atom 状态在测试用例间泄漏的测试套件。
    • effect 不是惰性的:它会在创建时自行订阅,因此模块级的 effect(...) 会在导入时立即连接,并一直运行,直到调用 .unsubscribe()。
    • 标记将组件/功能作用域内的副作用提升到模块级,以便“将状态放入模型”的做法:这会导致副作用失去挂载/可见性作用域,并在功能关闭时继续运行(还可能持续触发定时器循环)。
    • 优先在功能边界启动功能作用域内的副作用,顺序如下:
      1. 路由加载器 / 路由 render 初始化(最佳):加载器或路由所有的初始化操作会在功能作用域打开时创建副作用;在路由卸载或作用域变化时中止/断开连接。
      2. 功能模型上显式命名的 init / start 操作:在功能打开时调用一次(灯箱打开、面板挂载、会话开始)。
      3. 组件挂载 / ref 清理(可接受,但架构较弱):在已挂载的作用域中创建副作用,并在销毁时调用 .unsubscribe()。
    • 仅当副作用不会直接或间接读取钩子目标 atom 时,withConnectHook + effect 才是合适的模式。如果副作用依赖于拥有连接钩子的同一个 atom,就可能形成无限连接/订阅循环。标记 target.extend(withConnectHook(() => effect(() => target()))) 等类似代码。
    • 适合使用 withConnectHook 时,将其附加到副作用不应依赖的作用域锚点上(例如,为读取 slideshowPlaying 的幻灯片计时器使用 lightboxOpen,而不是在 slideshowPlaying 本身上使用 withConnectHook)。
    • 标记对已有模块级 effect 调用 .subscribe() / .unsubscribe() 的组件:该副作用在创建时已自行订阅,因此组件订阅是多余的,而且原有的自行订阅会泄漏(副作用不会在组件卸载时断开连接)。
    • 将外部推送源(ResizeObserver、IntersectionObserver、matchMedia、套接字)桥接到由连接驱动的 atom 时,优先使用 reatomObservable,而不是用 effect 接入观察器并写入相邻的结果 atom。
    • 对不依赖钩子目标的惰性外部订阅/轮询,使用 withConnectHook;并验证清理和中止行为。
    • 不要使用 withChangeHook 将 atom 与其他 atom 同步;优先使用 computed 或 withComputed。
  6. 路由:

    • 路由数据优先使用 reatomRoute 加载器;加载器是带有 withAsyncData 的异步计算值。
    • 路由加载器是计算值工厂的精髓:路由匹配结果和参数确定作用域,加载器创建的模型会在作用域变化时被替换。
    • 路由加载器或路由专属初始化也是启动功能作用域副作用(计时器、轮询、会话连接)的首选位置。避免模块级副作用,也不要在副作用读取的原子上使用 withConnectHook。
    • 标记那些手动检查 route.match() 并返回 null 的组件;优先使用路由的 render 选项和布局/出口。
    • 类型重要时,使用 schema 验证 URL 参数/搜索参数,并转换字符串参数,不要假定它们是数字。
    • 确认路由导航使用 .go(...),并在需要 SPA 拦截时让链接使用 .path(...)。
    • 路由路径不带前导 /;route.go() 接受参数,而不是路径字符串。
    • 加载器接收一个合并后的参数/搜索参数对象。(params, search) 是错误的。
    • render 是路由选项;构造完成后,route.render 是计算值输出,不是可赋值的回调。
    • 在 route.render(self) 内创建并传递给 UI 的回调仍需使用 wrap(...)。
    • 身份验证、重定向和功能门控应放在 params()/父级守卫中,而不是可为 null 的加载器负载中。
    • 守卫或 URL 钩子中的重定向必须具备幂等性,并在调用 .go(..., true) 前确认 URL 归属。
    • 对于布局下的索引路由,优先使用 exact() 判断活动状态;对于后代路由,match() 仍会返回 true。
  7. 中止、采样和并发:

    • take(...) 和 onEvent(...) 会返回 promise;在异步 action/effect 中应使用 await wrap(...)。
    • race(...) 需要 abortVar.createAndRun 返回的受控 promise,而不是普通 promise。
    • 在可中止的 Reatom 上下文中执行 fetch 时,应传入 signal: abortVar.require().signal。
    • 对于返回同步模型且包含异步 action/effect/轮询的计算值工厂,应在外层工厂使用 withAbort();带有 withAsyncData 的异步计算值/加载器已支持中止。
    • 检查工厂依赖:每次读取都可能重新创建内部模型。拆分易变输入、将推导移出,或在只应重建部分输入对应的作用域图时使用 peek/memo。
    • 对于类似防抖的行为,优先使用 withAbort() 和 await wrap(sleep(ms))。
    • 标记组件/effect 中的计时器管理(setTimeout、setInterval、本地计时器句柄、手动 clear*、卸载时的 try/catch)。对于由状态驱动的计时器,优先使用可中止的 effect(async () => { await wrap(sleep(ms)); ... });对于防抖/节流命令,优先使用 action(...).extend(withAbort())。
    • 除非流程明确需要,否则不要将中止导致的拒绝视为业务错误。
  8. 表单:

    • 表单优先使用 reatomForm、reatomFieldSet 和 reatomField。
    • 异步验证应使用 wrap,依赖验证可以响应式地读取其他字段。
    • 提交处理器应抛出错误,以供 submit.error() 使用,并明确指定负载类型。
    • 路由关联的表单通常应在路由/作用域工厂中创建,而不是创建为共享的模块级单例。
    • 面向用户的字段值优先使用 field.value()/field.change(value)。field() 是底层状态。
    • 将提交变更放在 reatomForm({ onSubmit }) 中,并调用 form.submit();单独的原始提交 action 可能绕过验证。
  9. 持久化和 URL 同步:

    • 优先使用 Reatom 辅助工具,例如 withLocalStorage、withSessionStorage、withSearchParams,或针对存储的持久化扩展,而不是临时编写副作用。
    • 检查 URL 状态和持久化状态的解析/序列化行为,尤其是默认值和无效输入。
    • 每个 atom 的持久化键必须唯一;标记不同模型间重复的键,以及未递增 version 并执行迁移的结构变更。
    • 对于缓存查询,检查 withCache 选项是否符合预期:swr 语义、staleTime/长度限制,以及 ignoreAbort 的默认值(仅在参数为空时为 true)。标记非幂等操作上的缓存。
  10. 迁移正确性:

  • 标记 v3 时代的过时 API:ctx.schedule、ctx.spy、ctx.get、reatomAsync、reatomResource、reaction、atom.onChange、onConnect、withConcurrency 和 onCtxAbort。
  • 优先使用当前的等效 API:wrap、直接读取 atom、peek、action(...).extend(withAsync())、computed(...).extend(withAsyncData())、effect、withChangeHook、withConnectHook、withAbort 和 abortVar.subscribe。
  1. 文档和示例:
  • 文档不得将反模式作为推荐代码展示。
  • 如果展示错误代码,请清楚地标注,并立即提供推荐的 Reatom 版本。
  • 检查导入项是否与示例相符、代码片段中使用的标识符是否存在,以及说明中的说法是否与代码一致。
  • 标记标题、说明、代码和 API 行为之间的不一致。
  • 示例应避免不安全的类型转换、匿名 atom/action,以及掩盖关键 Reatom 模式的虚构 API。
  1. React 和适配器:
  • 调用 atom getter 的组件应使用 reatomComponent;useAtom 的返回值是普通值,不是可调用的 getter。
  • 手写的 UI 回调若读取/写入 atom 或调用 action,就需要使用 wrap(...),第三方控件的回调也不例外。
  • 不要在普通函数组件的 JSX 中直接调用 wrap(...);应在 Reatom 上下文中创建包装后的回调,或将其传入组件。
  • 将 atom 作为 props 传递符合 Reatom 的解耦设计。不要凭 Redux 的经验否定这种做法。
  • DOM ref 回调、观察器通知以及其他非 Reatom 入口中写入 atom 的操作都需要 Reatom 上下文:context.start(() => ...)、wrap(...) 或 onEvent(...)。标记从 ref 或 ResizeObserver 回调中直接调用 atom.set 的情况。
  1. 浏览器资源所有权:
  • 将 URL.createObjectURL 的结果视为创建它的异步 computed(或 connect hook)所拥有的资源;当该 computed 重新运行/中止,或模型断开连接时,必须撤销 URL。标记将对象 URL 作为普通字符串存储且没有撤销路径的情况。
  • 将 createImageBitmap/ImageBitmap.close()、OffscreenCanvas、Worker(以及 worker 池)、ResizeObserver/IntersectionObserver 和 matchMedia 监听器的生命周期绑定到 Reatom 生命周期(异步 computed 中止、withConnectHook/withDisconnectHook 清理、abortVar.subscribe 或 onEvent)。
  • 如果 withConnectHook/withDisconnectHook/异步 computed 中止已能表达资源所有权,应标记那些需要调用方自行记得调用的手写 dispose()/cleanup() 方法。
  • 共享的 worker/解码器池应建模为具名服务模型,并显式处理连接/断开连接,以便文件夹/会话/路由重置时终止过期任务,避免泄漏。
  1. 模块结构和循环依赖:
  • 标记在 action/effect 中使用 await import('./peer') 或 require('./peer')、仅为打破循环导入的情况。动态 import() 用于代码拆分,而不是打破循环依赖;它还会将同步流程变为异步,并使依赖关系无法通过追踪看到。
  • 真正的解决方法是重构:将共享协调逻辑移至更高层的编排模块、反转依赖,或传入依赖。必须同时处理多个关联模块的重置/拆卸逻辑,通常应放在静态导入这些模块的编排 action 中。

文档不一致检查

审阅文档、教程、README、生成的摘要或示例时,请主动查找以下不一致之处:

  • 声称是“查询/资源/数据加载”,但代码使用 effect、组件生命周期、refs 或手动状态原子,而不是 computed(...).extend(withAsyncData())。
  • 声称是“变更/命令”,但代码使用 async computed 执行非幂等写入,而不是 action(...).extend(withAsync())。
  • 声称“可感知中止”或“竞态安全”,但代码缺少 withAbort、withAsyncData、路由加载器行为,或没有用 wrap 包裹等待中的工作。
  • 声称 Reatom 同步写入已被包装,但代码使用 wrap(() => atom.set(...)),却没有调用或传递返回的函数。
  • 声称代码通过 wrap 保留了上下文,但在 action/effect/computed 中使用 wrap(() => atom.set(...))(),而直接调用 atom.set(...) 本来就会在 frame 中运行。
  • 声称是“可中止的 fetch”,但代码没有向 fetch 传入 signal: abortVar.require().signal。
  • 声称使用 .status(),但 action/computed 没有通过 { status: true } 扩展。
  • 声称是“路由加载器”,但数据获取放在渲染的组件中,或受 route.match() 守卫。
  • 展示路由加载器参数,但代码使用 (params, search),而不是一个合并后的对象。
  • 声称是路由重定向/身份验证门控,但代码返回可空的加载器数据,而不是在 params() / 父级守卫中进行拦截。
  • 声称使用“当前版 Reatom”,但代码片段使用旧版 ctx、reatomResource、reatomAsync、reaction 或 onConnect。
  • 声称模型包含相对的状态/操作,但代码片段导出彼此独立的同级原子/操作,而没有通过 .extend 将它们分组。
  • 展示原子/action 工厂,但函数命名为 get* / create*,而不是 reatom*。
  • 展示用 action(...) 包装的纯映射器/格式化器/规范化器,但函数既没有 IO,也不写入状态。
  • 将派生状态写成 computed(() => resolveX(model)),并使用只读取原子 getter 的普通 resolveX 助手——这种拆分间接调用没有非 Reatom 的复用价值。
  • 声称提交表单时会进行验证,但代码绕过 form.submit(),改用独立的原始提交 action。
  • 标题/正文说的是一个原子/action 名称,代码片段却使用另一个名称。
  • 代码片段遗漏 wrap、computed、withAsyncData、action、withAsync 或 reatomRoute 等必要导入。
  • 示例将路由/搜索参数用作带类型的数字,却没有通过 schema 转换/强制转换。
  • 示例展示了错误代码,却没有标注为“错误/问题”,也没有在附近提供修正版本。
  • 声称 effect“作用域限定在屏幕/功能范围内”,但它是模块级 effect(...),在导入时自行订阅,只是在名义上由组件重新订阅。
  • 声称“将状态移入模型”,但此举把挂载作用域的 effect 变成了急切执行的模块级 effect,导致生命周期语义不同。
  • 声称 effect“作用域限定在连接钩子内”,但 effect 会直接或间接读取钩子目标原子,这可能导致无限的连接/订阅循环。
  • 声称“没有泄漏/卸载时会清理”,但对象 URL、位图、worker 或观察器依赖手动调用 dispose(),而不是由连接钩子/中止机制负责管理。
  • 声称模块彼此解耦,但循环依赖被隐藏在 action 内部的 await import() / require() 后面。

常见不一致及修复方法

使用命令式 Effect 实现查询

问题:

const users = atom<User[]>([], 'users')

effect(async () => {
  users.set(await api.getUsers(page()))
}, 'users.fetch')

修复:

const users = computed(async () => {
  return await wrap(api.getUsers(page()))
}, 'users').extend(withAsyncData({ initState: [] }))

原因:查询数据应当是惰性的、可感知中止的,并提供 data、ready、error、status、retry 和 reset。

wrap 链式调用错误

问题:

const response = await wrap(fetch(url)).then((res) => res.json())
data.set(response)

修复:

const response = await wrap(fetch(url))
const payload: Payload = await wrap(response.json())
data.set(payload)

原因:Reatom 能感知每个异步边界,并保留追踪/取消能力。

将 wrap 用作语句(未调用函数)

问题:

} finally {
  wrap(() => activeRequests.set((count) => count - 1))
}

修复:

} finally {
  activeRequests.set((count) => count - 1)
}

回调传递给外部 API 时的替代修复:

button.addEventListener(
  'click',
  wrap(() => counter.set((value) => value + 1)),
)

原因:wrap(fn) 会为外部调用方装饰 fn;它不会执行 fn。在 action/effect/异步 computed 内部——包括 await wrap(...) 之后的 finally 中——上下文已恢复;请直接调用 atoms。

无意义的 wrap IIFE

问题:

} finally {
  wrap(() => activeRequests.set((count) => count - 1))()
}

修复:

} finally {
  activeRequests.set((count) => count - 1)
}

原因:在 Reatom 框架内,wrap(() => ...)() 只是一次间接调用。仅在将回调交给 DOM/计时器/第三方代码时使用 wrap(fn);在异步边界处使用 await wrap(promise)。

异步边界后缺少 wrap

问题:

const save = action(async (form: FormState) => {
  const response = await fetch('/api/save', {
    method: 'POST',
    body: JSON.stringify(form),
  })
  savedId.set(await response.text())
}, 'form.save')

修复:

const save = action(async (form: FormState) => {
  const response = await wrap(
    fetch('/api/save', {
      method: 'POST',
      body: JSON.stringify(form),
    }),
  )
  const savedIdText: string = await wrap(response.text())
  savedId.set(savedIdText)
}, 'form.save')

原因:状态更新发生在异步工作之后,因此异步边界必须保留 Reatom 上下文。

回调在没有上下文时调用 Reatom

问题:

addEventListener('online', () => {
  online.set(true)
})

修复:

onEvent(globalThis, 'online', () => {
  online.set(true)
})

必须使用原始回调 API 时的替代修复:

addEventListener(
  'online',
  wrap(() => {
    online.set(true)
  }),
)

原因:回调也是异步入口。请保留上下文,或使用 Reatom 的可感知中止事件辅助工具。

等待的事件未使用 wrap

问题:

const confirm = action(async (button: HTMLButtonElement) => {
  await onEvent(button, 'click')
  confirmed.set(true)
}, 'confirm')

修复:

const confirm = action(async (button: HTMLButtonElement) => {
  await wrap(onEvent(button, 'click'))
  confirmed.set(true)
}, 'confirm')

原因:onEvent(...) 会返回一个 promise。在异步 action/effect 中通过 wrap 等待它。

可中止的 Fetch 未传入中止信号

问题:

const user = computed(async () => {
  const response = await wrap(fetch(`/api/users/${userId()}`))
  const payload: unknown = await wrap(response.json())
  return parseUser(payload)
}, 'user').extend(withAsyncData())

修复:

const user = computed(async () => {
  const response = await wrap(
    fetch(`/api/users/${userId()}`, {
      signal: abortVar.require().signal,
    }),
  )
  const payload: unknown = await wrap(response.json())
  return parseUser(payload)
}, 'user').extend(withAsyncData())

原因:withAsyncData、路由加载器、withAbort 和可感知中止的 effect 可以取消 Reatom 框架;fetch 应接收相同的中止信号。

未启用便使用状态

问题:

const submit = action(async () => {
  await wrap(api.save(form()))
}, 'form.submit').extend(withAsync())

const status = submit.status()

修复:

const submit = action(async () => {
  await wrap(api.save(form()))
}, 'form.submit').extend(withAsync({ status: true }))

const status = submit.status()

替代修复:

const ready = submit.ready()
const pending = submit.pending()
const error = submit.error()

原因:异步扩展默认禁用 status。仅在需要完整状态模型时使用 { status: true }。

恒等 Action

问题:

const query = atom('', 'search.query')
const setQuery = action((next: string) => query.set(next), 'search.query.set')

修复:

const query = atom('', 'search.query')
query.set('next value')

原因:简单的本地更新不需要转发 action。请将 action 用于副作用和状态变更流程,而不是纯数据映射。

对普通辅助函数进行薄封装的 Computed

问题:

export function resolveDownloadUrl(image: ReatomImage): string {
  return image.fullImageUrl.data() ?? image.thumbnail.data()?.url ?? ''
}

const downloadUrl = computed(
  () => resolveDownloadUrl(imageModel),
  `${name}.display.downloadUrl`,
)

修复:

const downloadUrl = computed(
  () =>
    imageModel.fullImageUrl.data() ?? imageModel.thumbnail.data()?.url ?? '',
  `${name}.display.downloadUrl`,
)

当同一派生逻辑会在测试或非 Reatom 代码中复用时的替代修复:在普通数据(URL、DTO)上保留纯函数,而不是用于包含 atom 的模型;让 computed 将 atoms 映射为该形状。

原因:只委托给读取 atoms 的辅助函数的 computed 会增加间接层,却没有带来可追踪性上的好处。computed 的主体(或父级上的 withComputed)就是派生逻辑;普通辅助函数应作用于普通值,而不是作为 atoms 上的一层影子封装。

名称看起来像 getter 的 Atom 工厂

问题:

export const getFolderTreeNodeUi = (folderPath: string) => ({
  expanded: reatomBoolean(false, `folderTree.${folderPath}.expanded`),
  isSelected: computed(
    () => currentFolder()?.path === folderPath,
    `folderTree.${folderPath}.isSelected`,
  ),
})

修复:

export const reatomFolderTreeNodeUi = (folderPath: string) => ({
  expanded: reatomBoolean(false, `folderTree.${folderPath}.expanded`),
  isSelected: computed(
    () => currentFolder()?.path === folderPath,
    `folderTree.${folderPath}.isSelected`,
  ),
})

原因:Reatom 工厂会创建可追踪的 atoms 和 actions;reatom* 能表明这一约定,并与 reatomBoolean、reatomRoute 和 reatomForm 等核心辅助工具保持一致。普通的 get* / create* 名称会掩盖嵌套单元的生命周期和命名规则。

相对状态分散为同级导出项

问题:

export const search = atom('', 'search')
export const searchIsEmpty = computed(
  () => search().trim() === '',
  'search.isEmpty',
)
export const clearSearch = action(() => search.set(''), 'search.clear')

修复:

export const search = atom('', 'search').extend((target) => ({
  isEmpty: computed(() => target().trim() === '', `${target.name}.isEmpty`),
  clear: action(() => target.set(''), `${target.name}.clear`),
}))

原因:相对状态和 actions 应放在父模型上,就像 reatomBoolean 将布尔操作集中在一起,reatomRoute 则附加了 go、loader、render 和子路由辅助工具。

使用并行 UI 状态而非原子化

问题:

const users = atom<UserDto[]>([], 'users')
const selectedUserIds = atom<Set<string>>(new Set(), 'users.selectedIds')

修复:

type UserModel = UserDto & {
  selected: Atom<boolean>
}

const users = atom<UserModel[]>([], 'users').extend((target) => ({
  fromDto(items: UserDto[]) {
    target.set(
      items.map((item) => ({
        ...item,
        selected: atom(false, `users#${item.id}.selected`),
      })),
    )
  },
}))

原因:每个项目的可变状态应放在项目附近,以避免并行结构和大范围列表更新。

手动渲染路由

问题:

export function UsersPage() {
  if (!usersRoute.match()) return null
  return <Users />
}

修复:

export const usersRoute = layoutRoute.reatomRoute({
  path: 'users',
  render() {
    return <Users />
  },
})

原因:路由 render 会处理挂载、精确匹配、加载器、布局和插槽。

组件作用域的 Effect 提升到模块作用域(永久生效)

问题:

// models/slideshow.ts
export const slideshowAutoAdvance = effect(async () => {
  while (slideshowPlaying()) {
    await wrap(sleep(slideshowInterval()))
    navigateLightbox(1)
  }
}, 'slideshow.autoAdvance')

// components/Slideshow.tsx
ref={() => {
  const stop = slideshowAutoAdvance.subscribe()
  return stop
}}

修复(首选——路由/功能初始化):

// models/lightbox.ts
export const openLightbox = action((model: GalleryImageModel) => {
  lightboxImage.set(() => model)
  lightboxOpen.setTrue()
  startSlideshowSession()
}, 'openLightbox')

export const startSlideshowSession = action(() => {
  effect(async () => {
    while (slideshowPlaying()) {
      await wrap(sleep(slideshowInterval()))
      navigateLightbox(1)
    }
  }, 'slideshow.autoAdvance')
}, 'slideshow.startSession')

替代修复(可接受——在功能组件中挂载):

// components/Slideshow.tsx
ref={() => {
  const {unsubscribe} = effect(async () => {
    while (slideshowPlaying()) {
      await wrap(sleep(slideshowInterval()))
      navigateLightbox(1)
    }
  }, 'slideshow.autoAdvance')

  return unsubscribe
}}

原因:effect(...) 会在创建时自行订阅,因此模块级 effect 会立即连接且永不断开;组件上额外的 .subscribe() 是多余的,原始的自订阅也会造成泄漏。应在功能边界启动 effect(路由加载器、显式初始化 action 或组件挂载),而不是将其设为永远连接的模块单例。

在 withConnectHook 内的 Effect 读取其自身依赖(无限循环)

问题:

export const slideshowPlaying = reatomBoolean(false, 'slideshowPlaying').extend(
  withConnectHook(() => {
    effect(async () => {
      while (slideshowPlaying()) {
        await wrap(sleep(slideshowInterval()))
        navigateLightbox(1)
      }
    }, 'slideshow.autoAdvance')
  }),
)

修复(将 Effect 锚定到其不会读取的作用域):

export const lightboxOpen = reatomBoolean(false, 'lightboxOpen').extend(
  withConnectHook(() => {
    effect(async () => {
      while (peek(slideshowPlaying)) {
        await wrap(sleep(slideshowInterval()))
        navigateLightbox(1)
      }
    }, 'slideshow.autoAdvance')
  }),
)

更好的修复(在功能打开时显式初始化):

export const startSlideshowSession = action(() => {
  effect(async () => {
    while (slideshowPlaying()) {
      await wrap(sleep(slideshowInterval()))
      navigateLightbox(1)
    }
  }, 'slideshow.autoAdvance')
}, 'slideshow.startSession')

原因:目标有订阅者时会运行连接钩子。如果嵌套的 Effect 读取同一个目标(直接读取或通过计算值读取),连接/订阅就可能不断反馈,造成无限循环。可以将钩子附加到不同的作用域锚点,仅在门控检查时使用 peek,或者改为从路由加载器/初始化操作/组件挂载时启动 Effect。

对象 URL 被泄漏为普通字符串

问题:

const previewUrl = computed(() => {
  const blob = imageBlob.data()
  return blob ? URL.createObjectURL(blob) : ''
}, 'image.previewUrl')

修复:

const previewUrl = computed(async () => {
  const blob = await wrap(imageBlob())
  if (!blob) return ''
  const url = URL.createObjectURL(blob)
  abortVar.subscribe(() => URL.revokeObjectURL(url))
  return url
}, 'image.previewUrl').extend(withAsyncData({ initState: '' }))

原因:URL.createObjectURL 会分配资源。将撤销操作关联到所属计算值的中止/断开,以便模型重新运行或断开连接时释放 URL,而不是每次重新计算都泄漏一个 URL。

常见问题的简要修复方法
  • 模块级 effect(...) → 导入时立即执行;应从路由加载器、显式初始化操作或组件挂载时启动,而不是将其作为永久连接的单例。
  • withConnectHook + 读取钩子目标的 effect → 连接循环无限进行;使用不同的作用域锚点、在门控检查时使用 peek,或改为从路由/初始化/组件挂载时启动。
  • 使用 await import('./peer')/require('./peer') 打破循环依赖 → 重构模块或使用编排操作;动态导入用于代码分割。
  • 手动编写对象 URL/位图/观察器/工作线程的 dispose() → 将清理操作关联到 withConnectHook/withDisconnectHook 或异步计算的中止。
  • 从 DOM ref/观察器回调中调用 atom.set → 将 wrap(() => atom.set(...)) 传给回调 API,或使用 context.start(() => atom.set(...))。
  • 将 wrap(() => atom.set(...)) 作为独立语句 → 不会产生任何效果;直接调用 atom.set(...),或在外部传入 wrap(fn)。
  • 在操作/Effect/计算值中使用 wrap(() => atom.set(...))() → 多此一举;直接调用 atom.set(...)。
  • 使用普通的 resolveX(会读取 atom)构造 computed(() => resolveX(model)) → 将逻辑内联到 computed 中,或通过父级上的 withComputed 附加。
  • 使用 counter(5) 写入 → 使用 counter.set(5);对响应式 atom 使用位置参数调用来写入会抛出错误。
  • 不带 cacheParams 调用 action.retry() → 使用 withAsync({ cacheParams: true }),或改为重试 computed。
  • 使用 .extend(withCache(), withAsync()) → 调整顺序:先 withAsync/withAsyncData,再 withCache。
  • 之后将 getCalls(someAction) 作为数据读取 → 操作调用列表会在下一个 tick 清空;将数据负载存储在 atom 中。
  • 使用 reatomMap()().set(k, v)/reatomArray()().push(x) → 使用原语提供的操作;原地变更会跳过失效处理。
  • .set(...) 后立即进行同步断言,并期望订阅者已触发 → 等待一个微任务;通知会排入队列。
  • 在测试间共享状态 → 在 beforeEach 中调用 context.reset(),或使用 context.start 创建作用域。
  • 使用 status.isPending() → 使用 status.isPending;状态标志是属性。
  • 使用 status.error → 使用 target.error();错误保留在异步目标 atom 中。
  • 使用 async loader(params, search) → 使用 async loader({ q, userId });加载器接收一个合并后的 params/search 对象。
  • 使用可空的身份验证加载器 → 使用 params() { return allowed ? {} : null };守卫应在加载器运行前阻止路由所有权。
  • 使用 route.go('/users') → 使用 usersRoute.go(params);路由路径声明时不带前导 /。
  • 模块级路由表单单例 → 使用路由/作用域工厂,或在表单生命周期随路由变化时由加载器创建表单。
异步扩展在缓存之后添加

问题:

const users = computed(async () => {
  return await wrap(api.getUsers())
}, 'users').extend(withCache(), withAsyncData())

修复:

const users = computed(async () => {
  return await wrap(api.getUsers())
}, 'users').extend(withAsyncData(), withCache())

原因:withAsync/withAsyncData 不允许在 withCache 之后附加(运行时会抛出 ReatomError),而且异步中间件必须能够观察缓存命中,才能确保 pending、data 和 status 保持一致。

将操作调用列表当作持久状态

问题:

const addToast = action((toast: Toast) => toast, 'addToast')
const toasts = computed(
  () => getCalls(addToast).map((call) => call.payload),
  'toasts',
)

修复:

const toasts = atom<Toast[]>([], 'toasts').extend(
  withActions((target) => ({
    add: (toast: Toast) => target.set((list) => [...list, toast]),
  })),
)

原因:操作状态是可自动清除的数组,会在下一个清理 tick 中清空。它用于在事务中对调用作出响应,而不是用于存储;持久数据应存放在 atom 中。

过时的 v3 API

问题:

const resource = reatomResource(async (ctx) => {
  const response = await ctx.schedule(fetch('/api/users'))
  return response.json()
}, 'users')

修复:

const users = computed(async () => {
  const response = await wrap(fetch('/api/users'))
  return await wrap(response.json())
}, 'users').extend(withAsyncData())

原因:当前 Reatom 使用隐式上下文、wrap 和异步计算资源。

问题格式

每个问题都使用以下格式:

- [Severity] `path-or-symbol`: Problem statement.
  Why it matters: concrete Reatom rule or failure mode.
  Fix: specific code-level change.

严重性指南:

  • 严重:状态不正确、异步上下文丢失、取消/竞态问题、路由行为异常,或无法使用的过时 API。
  • 高:绕过推荐的 Reatom 模式,可能导致资源泄漏、过早请求、数据过时或文档误导。
  • 中:可维护性、可追溯性、命名、原子化或测试质量明显较弱。
  • 低:风格/文档清晰度问题,可能使后续 Agent 困惑,但不太可能破坏行为。

如果没有遗留问题,请直接说明,并列出剩余风险,尤其是未经测试的异步取消、路由加载器行为或尚未执行的文档示例。

最终审查清单

  • 是否检查了 Agent 更改的代码和文档?
  • 是否依据 REFERENCE.md 核对声明,而非套用通用的 React/Solid/Vue 习惯?
  • 是否检查了每个 await/回调边界之后的异步上下文和 wrap?
  • 是否指出 Reatom 调用帧中的裸 wrap(() => ...) 语句和无意义的 wrap(() => ...)() IIFE?
  • 是否质疑了命令式请求、手动路由和并行的可变状态?
  • 是否指出将 action 用于没有 IO 或状态写入的纯映射器?
  • 是否指出了 computed(() => helper(model)) 这类薄封装,而其中的 helper 只读取 atom getter?
  • 是否指出了以 get*/create* 而非 reatom* 命名的 atom/action 工厂?
  • 是否检查了 effect 生命周期:模块级 effect 的急切执行、功能初始化(路由加载器/init action/组件挂载)、connect hook 的依赖陷阱,以及冗余的组件订阅?
  • 是否检查了对象 URL、位图、worker 和观察器是否由 Reatom 生命周期管理,而非手动 dispose()?
  • 是否检查了 .extend(...) 顺序(async 在 cache 之前)、retry/status 的选项要求,以及扩展中的键冲突?
  • 是否检查了写入语法(.set 与位置参数调用)、对集合原语的原地修改,以及对临时 action 调用列表的依赖?
  • 是否检查了时序假设:排队的通知、取消订阅后的 computed 重新验证,以及通过 context.reset() 隔离测试上下文?
  • 是否确认示例可直接复制,且没有隐藏关键导入?
  • 是否至少考虑过能验证 Reatom 行为的测试或示例,避免未经检查便批准?

评论

支持 Markdown