关于 reatom-review
reatom-review 是用于按 Reatom v1001 规范审查 Agent 工作的技能,面向代码审查与验证。它强调依据权威参考资料核对行为、API、测试和状态管理模式,并优先报告具体问题。
主要功能
- 01按 Reatom v1001 规范审查
- 02核查异步与上下文传播
- 03检查状态建模和命名
- 04审视副作用与订阅生命周期
- 05基于参考资料提出具体修正
适用场景
- 01检查 Agent 提交的 Reatom 代码
- 02发现过时 API 和误导性文档
- 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 代码;还要审查上下文传播、取消、惰性、命名和订阅。
必查项
-
异步读取和查询:
- 对幂等的读取/查询数据,应使用
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。
- 对幂等的读取/查询数据,应使用
-
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 上下文中运行。
- 在离开 Reatom 框架的异步边界(fetch、计时器、DOM Promise 等)处使用
-
状态建模:
- 写入应通过
.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,应为其命名。
- 只将一种数据形状映射为另一种数据形状的函数——不进行 IO、不调用
- 标记薄 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;当在命名作用域之外读取属于错误时则抛出异常。
- 写入应通过
-
命名与可追踪性:
- 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 和示例中的匿名原语。
-
副作用、钩子和订阅:
- 使用
computed派生状态,使用effect执行副作用。 computed是惰性的;请确认预期会加载的数据有订阅者,或存在明确的路由/渲染路径。- 未订阅的
computed会在每次读取时重新验证;已订阅的则会通过推送缓存。标记在热循环中读取开销较大的未订阅computed,以及热路径上没有订阅者的开销较大的computed——可考虑使用withMemo或添加订阅。 - 订阅者回调和队列副作用会异步刷新(微任务)。标记这类代码(尤其是测试):在
.set(...)后立即断言订阅者已同步触发;应等待一个微任务/sleep(0),或直接读取 atom。 - 使用默认全局上下文的测试必须通过
context.reset()(或context.start作用域)隔离状态。标记 atom 状态在测试用例间泄漏的测试套件。 effect不是惰性的:它会在创建时自行订阅,因此模块级的effect(...)会在导入时立即连接,并一直运行,直到调用.unsubscribe()。- 标记将组件/功能作用域内的副作用提升到模块级,以便“将状态放入模型”的做法:这会导致副作用失去挂载/可见性作用域,并在功能关闭时继续运行(还可能持续触发定时器循环)。
- 优先在功能边界启动功能作用域内的副作用,顺序如下:
- 路由加载器 / 路由
render初始化(最佳):加载器或路由所有的初始化操作会在功能作用域打开时创建副作用;在路由卸载或作用域变化时中止/断开连接。 - 功能模型上显式命名的
init/start操作:在功能打开时调用一次(灯箱打开、面板挂载、会话开始)。 - 组件挂载 /
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。
- 使用
-
路由:
- 路由数据优先使用
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。
- 路由数据优先使用
-
中止、采样和并发:
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())。 - 除非流程明确需要,否则不要将中止导致的拒绝视为业务错误。
-
表单:
- 表单优先使用
reatomForm、reatomFieldSet和reatomField。 - 异步验证应使用
wrap,依赖验证可以响应式地读取其他字段。 - 提交处理器应抛出错误,以供
submit.error()使用,并明确指定负载类型。 - 路由关联的表单通常应在路由/作用域工厂中创建,而不是创建为共享的模块级单例。
- 面向用户的字段值优先使用
field.value()/field.change(value)。field()是底层状态。 - 将提交变更放在
reatomForm({ onSubmit })中,并调用form.submit();单独的原始提交 action 可能绕过验证。
- 表单优先使用
-
持久化和 URL 同步:
- 优先使用 Reatom 辅助工具,例如
withLocalStorage、withSessionStorage、withSearchParams,或针对存储的持久化扩展,而不是临时编写副作用。 - 检查 URL 状态和持久化状态的解析/序列化行为,尤其是默认值和无效输入。
- 每个 atom 的持久化键必须唯一;标记不同模型间重复的键,以及未递增
version并执行迁移的结构变更。 - 对于缓存查询,检查
withCache选项是否符合预期:swr语义、staleTime/长度限制,以及ignoreAbort的默认值(仅在参数为空时为 true)。标记非幂等操作上的缓存。
- 优先使用 Reatom 辅助工具,例如
-
迁移正确性:
- 标记 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。
- 文档和示例:
- 文档不得将反模式作为推荐代码展示。
- 如果展示错误代码,请清楚地标注,并立即提供推荐的 Reatom 版本。
- 检查导入项是否与示例相符、代码片段中使用的标识符是否存在,以及说明中的说法是否与代码一致。
- 标记标题、说明、代码和 API 行为之间的不一致。
- 示例应避免不安全的类型转换、匿名 atom/action,以及掩盖关键 Reatom 模式的虚构 API。
- 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的情况。
- 浏览器资源所有权:
- 将
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/解码器池应建模为具名服务模型,并显式处理连接/断开连接,以便文件夹/会话/路由重置时终止过期任务,避免泄漏。
- 模块结构和循环依赖:
- 标记在 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 行为的测试或示例,避免未经检查便批准?