reatom-review
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReatom Review
Reatom 审查
Use this skill to validate an agent's work against Reatom v1001 practices. Be skeptical: the goal is to find incorrect behavior, misleading docs, stale API usage, weak tests, and patterns that only look plausible.
使用此技能验证Agent的工作是否符合Reatom v1001的实践规范。保持质疑态度:目标是找出错误行为、误导性文档、过时API用法、薄弱测试以及看似合理但存在问题的模式。
Reference
参考资料
Before validating API usage, extension options, or documentation claims, also load the skill and read the relevant sections of REFERENCE.md. It is the canonical v1001 API reference. For implementation (not review), use the skill directly.
reatomreatom在验证API用法、扩展选项或文档声明之前,请同时加载技能并阅读REFERENCE.md的相关章节。这是v1001 API的权威参考。若要实现而非审查代码,请直接使用技能。
reatomreatomReview Stance
审查立场
- Lead with findings. Do not praise before checking correctness.
- Criticize the reviewed work, not the author.
- Treat REFERENCE.md as the local source of truth when it conflicts with generic frontend habits.
- Quote concrete files/symbols and explain the failure mode.
- Prefer one precise fix over vague advice.
- If a change is acceptable only with a special reason, ask for that reason or mark it as a risk.
- Do not approve Reatom code just because TypeScript compiles; review context propagation, cancellation, laziness, naming, and subscriptions.
- 先列出发现的问题。在确认正确性之前不要先表扬。
- 批评审查的工作内容,而非作者本人。
- 当REFERENCE.md与通用前端习惯冲突时,以此文档为本地事实依据。
- 引用具体的文件/符号并解释失败模式。
- 优先给出精准的修复方案,而非模糊建议。
- 如果某项变更仅在特殊理由下才被接受,请要求提供该理由或标记为风险项。
- 不要仅因为TypeScript编译通过就批准Reatom代码;需审查上下文传播、取消机制、惰性执行、命名规范和订阅逻辑。
Mandatory Checks
强制检查项
-
Async reads and queries:
- For idempotent read/query data, expect .
computed(async () => ...).extend(withAsyncData(...)) - Flag mount-time fetches, fetches, refs, component-local async state, or imperative loaders unless the code has a clear non-query reason.
effect - For mutations/commands, expect , plus
action(async () => ...).extend(withAsync(...))or transactions when needed.withAbort - is available only when
.status()/withAsyncenableswithAsyncData. Otherwise prefer{ status: true },.ready(), and.pending()..error() - on an action requires
.retry(); without itwithAsync({ cacheParams: true })throws at call time. Computeds can retry without options.retry - Extension order matters: /
withAsyncmust be applied beforewithAsyncData(attachingwithCacheafterwithAsyncthrows). Review everywithCachechain for ordering, not just presence..extend(...) - Async helper atoms are getters: use ,
.data(),.ready(), and.error(). Do not render or destructure atom objects as inert values.submit.error() - Status flags are properties, for example , not functions. Use the target's
status.isPendingatom; do not invent.error().status.error
- For idempotent read/query data, expect
-
and async context:
wrap- Use at async boundaries that leave the Reatom frame (fetch, timers, DOM promises, etc.). After a wrapped await, the continuation is back in context — call atoms/actions directly.
await wrap(promise) - Use only when
wrap(fn)is passed to an external caller (DOM listener, timer, third-party callback). It returns a decorated function; it does not callfn. Flag barefnwith no assignment/pass-through to an external callback.wrap(() => atom.set(...)) - Flag pointless inside actions/effects/async computeds — an immediate wrapped IIFE adds nothing; just call
wrap(() => atom.set(...))()in the Reatom frame.atom.set(...) - Flag ; prefer wrapping the whole promise chain or wrapping each awaited step.
await wrap(fetch(url)).then(...) - Flag callbacks, DOM callbacks, timers,
.then(...), and external event listeners that call Reatom state withoutrequestAnimationFrameorwrap.onEvent - Check after every : if later code calls atoms/actions from an async continuation, the awaited promise should usually be wrapped.
await - Prefer over raw event listeners when awaiting DOM or external events.
await wrap(onEvent(...)) - belongs in Reatom-aware actions/effects/computeds/callbacks, not inside plain reusable API helpers.
wrap() - Downleveled async/await can break context propagation. Flag build/test targets that transform async code to chains when strict context errors appear.
.then() - Do not ask to wrap callbacks passed into Reatom hooks such as ; hooks already run in Reatom context.
withCallHook
- Use
-
State modeling:
- Writes go through . Calling a reactive atom with arguments (
.set(...)) throws; calling acounter(5)with arguments throws. Flag any positional-call writes; reads are zero-arg calls.computed - Action state (,
getCallsreturn list) is ephemeral — it is cleared in the next cleanup queue tick. Flag code that stores or later reads an action's call list as durable state; persist payloads into atoms instead.action() - Collection primitives (,
reatomMap,reatomSet,reatomArray,reatomRecord) update through their actions/immutable methods. Flag in-place mutation of their state (reatomLinkedList,map().set(...)): it skips invalidation and corrupts equality checks.array().push(...) - cannot replace the atom reference and cannot override existing keys — colliding method names throw at runtime. Flag extensions whose assigned keys shadow
.extend(...),set,subscribe, or earlier extension methods.extend - Mutable fields inside dynamic objects should be atomized.
- Flag normalized parallel UI state like separate ,
selectedIds, or edit maps when item-local atoms would be clearer.checkedIds - Action vs pure transform:
- A function that only maps one data shape to another — no IO, no , no other action calls — is not an action. Use a plain function or
atom.set.computed - A function that performs side effects (network, storage, timers, DOM, logging) or changes Reatom state (, calling actions) is an action and should be named.
atom.set
- A function that only maps one data shape to another — no IO, no
- Flag thin computed wrappers: a where
computed(() => helper(model))only reads atom getters on the model and returns a derived value. The helper duplicates the computed without adding reuse outside Reatom. Put the derivation in the computed body, or attach it withhelper/withComputedon the parent — do not split into a plain helper plus a pass-through computed..extend - Flag plain helpers that take atom-bearing models and call / atom getters for reactive derivations. They hide the reactive graph, invite thin computed wrappers, and are unsafe if called outside a computed/action frame.
.data() - Direct is fine for local/simple updates. Flag "identity" actions that only forward values to atoms.
atom.set - Complex transformations with side effects and multi-step flows should be actions with names.
- Relative states and actions should be grouped on their parent with or
.extend(...), not scattered as sibling exports.withActions(...) - Follow core patterns like and
reatomBoolean: create the parent atom, then attach related methods, child computeds, loaders, route factories, and helpers throughreatomRoute..extend - For scoped state, consider the computed factory pattern: a reads the scope key and returns the atoms/actions/forms for that scope, so changing the key replaces the inner graph.
computed - Computed factories are general, not router-only. Look for them around selected entities, edit sessions, modals, tabs, and any named unit of work with its own state lifetime.
- Choose the factory contract intentionally: return when inactive scope is ordinary state, or throw when reading outside the named scope is a bug.
null
- Writes go through
-
Naming and traceability:
- Atoms, computed values, effects, and actions should be named.
- Atom/action/model factories must use the prefix (for example
reatom*,reatomFolderTreeNodeUi,reatomGalleryImage), notreatomUser,get*, or other generic verbs. Flag factory functions that allocate named atoms, computeds, effects, or actions but read like plain getters or constructors.create* - Nested/dynamic names should preserve structure, for example ,
users.page, orusers#${id}.name.${target.name}.ready - Prefix the trace name of hot-path or noisy relative states and actions with on the segment: atoms, computed, effects, and actions tied to pointer move, scroll, resize, or animation ticks. Examples:
_,lightbox._panMove,lightbox._controlsActivity,lightbox._hideControlsAfterInactivity. Keeps logs readable.imageGrid._width - Flag anonymous primitives in shared models and examples.
-
Effects, hooks, and subscriptions:
- Use for derived state and
computedfor side effects.effect - is lazy; check that data expected to load has a subscriber or an explicit route/render path.
computed - Unsubscribed computeds revalidate on every read; subscribed ones are push-cached. Flag hot loops reading heavy unsubscribed computeds and expensive computeds without a subscriber on a hot path — consider or a subscription.
withMemo - Subscriber callbacks and queue effects flush asynchronously (microtask). Flag code (especially tests) that asserts a subscriber fired synchronously after ; await a microtask/
.set(...)or read the atom directly.sleep(0) - Tests sharing the default global context must isolate state with (or
context.reset()scoping). Flag test suites where atoms leak state between cases.context.start - is NOT lazy: it self-subscribes at creation, so a module-level
effectconnects eagerly at import and runs forever untileffect(...)..unsubscribe() - Flag component/feature-scoped effects lifted to module scope to "put state in the model": they lose mount/visibility scoping and keep running (and may loop on timers) while the feature is closed.
- Prefer starting feature-scoped effects at the feature boundary, in this order:
- Route loader / route init (best): the loader or route-owned init action creates the effect when the feature scope opens; abort/disconnect when the route unmounts or scope changes.
render - Explicit named /
initaction on the feature model: called once when the feature opens (lightbox open, panel mount, session start).start - Component mount / cleanup (acceptable but weaker architecture): create the effect in the mounted scope and call
refon teardown..unsubscribe()
- Route loader / route
- +
withConnectHookis a good pattern only when the effect does not read the hook target atom, directly or indirectly. If the effect depends on the same atom that owns the connect hook, it can create an infinite connect/subscribe loop. Flageffectand similar.target.extend(withConnectHook(() => effect(() => target()))) - When is the right tool, attach it to a scope anchor the effect must not depend on (for example
withConnectHookfor a slideshow timer that readslightboxOpen, notslideshowPlayingonwithConnectHookitself).slideshowPlaying - Flag a component that calls /
.subscribe()on an already module-level.unsubscribe(): the effect self-subscribed at creation, so the component subscription is redundant and the original self-subscription leaks (the effect never disconnects on unmount).effect - Prefer to bridge external push sources (
reatomObservable,ResizeObserver,IntersectionObserver, sockets) into a connection-driven atom, instead of anmatchMediathat wires the observer and writes a sibling result atom.effect - Use for lazy external subscriptions/polling that do not depend on the hook target; verify cleanup and abort behavior.
withConnectHook - Do not use to synchronize atoms with other atoms; prefer
withChangeHookorcomputed.withComputed
- Use
-
Routing:
- Prefer loaders for route data; loaders are async computeds with
reatomRoute.withAsyncData - A route loader is the quintessence of computed factory: route match/params name the scope, and loader-created models are replaced when that scope changes.
- Route loader / route-owned init is also the preferred place to start feature-scoped effects (timers, polling, session wiring). Avoid module-level effects and avoid on an atom the effect reads.
withConnectHook - Flag components that manually check and return
route.match(); prefer the routenulloption and layouts/outlets.render - Validate URL params/search with schemas when types matter, and transform string params instead of assuming numbers.
- Confirm route navigation uses and links use
.go(...)where SPA interception is expected..path(...) - Route paths have no leading ;
/takes params, not a path string.route.go() - Loader takes one merged params/search object. is wrong.
(params, search) - is a route option; after construction
renderis a computed output, not an assignable callback.route.render - Callbacks created inside and passed to UI still need
route.render(self).wrap(...) - Auth, redirect, and feature gates belong in / parent guards, not nullable loader payloads.
params() - Redirects in guards or URL hooks must be idempotent and prove URL ownership before .
.go(..., true) - For index routes under layouts, prefer for active state;
exact()stays true for descendants.match()
- Prefer
-
Abort, sampling, and concurrency:
- and
take(...)return promises; inside async actions/effects they should beonEvent(...).await wrap(...) - expects controlled promises from
race(...), not plain promises.abortVar.createAndRun - Fetches in abortable Reatom contexts should pass .
signal: abortVar.require().signal - For computed factories that return a synchronous model with async actions/effects/polling, expect on the outer factory; async computeds/loaders with
withAbort()already have abort support.withAsyncData - Check factory dependencies: every read can recreate the inner model. Split volatile inputs, move derivations out, or use /
peekwhen only some inputs should rebuild the scoped graph.memo - Prefer plus
withAbort()for debounce-like behavior.await wrap(sleep(ms)) - Flag component/effect timer bookkeeping (,
setTimeout, local timer handles, manualsetInterval, unmountclear*). Prefer abortabletry/catchfor state-driven timers andeffect(async () => { await wrap(sleep(ms)); ... })for debounce/throttle commands.action(...).extend(withAbort()) - Do not treat abort rejections as business errors unless the flow explicitly needs that.
-
Forms:
- Prefer ,
reatomForm, andreatomFieldSetfor forms.reatomField - Async validation should use , and dependent validation may read other fields reactively.
wrap - Submit handlers should throw errors for and keep payload types explicit.
submit.error() - Route-bound forms should usually be created in route/scoped factories, not as shared module-level singletons.
- Prefer /
field.value()for user-facing field values.field.change(value)is the underlying state.field() - Put submit mutations in and call
reatomForm({ onSubmit }); separate raw submit actions can bypass validation.form.submit()
- Prefer
-
Persistence and URL sync:
- Prefer Reatom helpers such as ,
withLocalStorage,withSessionStorage, or storage-specific persistence extensions over ad hoc effects.withSearchParams - Check parse/serialize behavior for URL state and persisted state, especially defaults and invalid input.
- Persist keys must be unique per atom; flag duplicated keys across models and shape changes without a bumped + migration.
version - For cached queries, check options against intent:
withCachesemantics,swr/length limits, andstaleTimedefaults (true only for empty params). Flag cache on non-idempotent actions.ignoreAbort
- Prefer Reatom helpers such as
-
Migration correctness:
- Flag v3-era stale APIs: ,
ctx.schedule,ctx.spy,ctx.get,reatomAsync,reatomResource,reaction,atom.onChange,onConnect, andwithConcurrency.onCtxAbort - Prefer current equivalents: , direct atom reads,
wrap,peek,action(...).extend(withAsync()),computed(...).extend(withAsyncData()),effect,withChangeHook,withConnectHook, andwithAbort.abortVar.subscribe
- Docs and examples:
- Docs must not present antipatterns as recommended code.
- If showing bad code, label it clearly and immediately provide the recommended Reatom version.
- Check that imports match examples, identifiers used in snippets exist, and narrative claims match the code.
- Flag mismatches between headings, prose, code, and API behavior.
- Examples should avoid unsafe casts, anonymous atoms/actions, and fake APIs that hide the important Reatom pattern.
- React and adapters:
- Components that call atom getters should be ;
reatomComponentresults are plain values, not callable getters.useAtom - Handwritten UI callbacks that read/write atoms or call actions need , including third-party control callbacks.
wrap(...) - Do not call directly in JSX of a plain function component; create wrapped callbacks inside a Reatom frame or pass them down.
wrap(...) - Passing atoms as props is valid Reatom decoupling. Do not reject it from Redux intuition.
- DOM callbacks, observer notifications, and other non-Reatom entry points that write atoms need a Reatom frame:
ref,context.start(() => ...), orwrap(...). Flag bareonEvent(...)from aatom.setorrefcallback.ResizeObserver
- Browser resource ownership:
- Treat results as resources owned by the async computed (or connect hook) that produced them; the URL must be revoked when that computed re-runs/aborts or the model disconnects. Flag object URLs stored as plain strings with no revocation path.
URL.createObjectURL - Tie /
createImageBitmap,ImageBitmap.close(),OffscreenCanvas(and worker pools),Worker/ResizeObserver, andIntersectionObserverlisteners to Reatom lifecycle (async-computed abort,matchMedia/withConnectHookcleanup,withDisconnectHook, orabortVar.subscribe).onEvent - Flag hand-rolled /
dispose()methods that callers must remember to invoke whencleanup()/withConnectHook/ async-computed abort already express ownership.withDisconnectHook - Shared worker/decoder pools should be named service models with explicit connect/disconnect, so folder/session/route resets terminate stale work instead of leaking it.
- Module structure and cycles:
- Flag or
await import('./peer')used inside an action/effect only to break a circular import. Dynamicrequire('./peer')is for code-splitting, not cycle breaking; it also turns a sync flow async and hides the dependency from tracing.import() - The real fix is restructuring: move shared coordination into a higher-level orchestration module, invert the dependency, or pass the dependency in. A reset/teardown that must touch several peers usually belongs in an orchestration action that imports them statically.
-
异步读取与查询:
- 对于幂等的读取/查询数据,应使用。
computed(async () => ...).extend(withAsyncData(...)) - 除非代码有明确的非查询理由,否则标记挂载时的请求、请求、refs、组件本地异步状态或命令式加载器。
effect - 对于变更/命令操作,应使用,必要时添加
action(async () => ...).extend(withAsync(...))或事务处理。withAbort - 仅当/
withAsync启用withAsyncData时,{ status: true }才可用。否则优先使用.status()、.ready()和.pending()。.error() - action上的需要
.retry();若无此配置,withAsync({ cacheParams: true })在调用时会抛出错误。Computed无需配置即可重试。retry - 扩展顺序至关重要:/
withAsync必须在withAsyncData之前应用(在withCache之后附加withCache会抛出错误)。审查每个withAsync链的顺序,而非仅检查是否存在。.extend(...) - 异步辅助atom是getter:使用、
.data()、.ready()和.error()。不要将atom对象作为惰性值渲染或解构。submit.error() - 状态标志是属性,例如,而非函数。使用目标的
status.isPendingatom;不要自行定义.error()。status.error
- 对于幂等的读取/查询数据,应使用
-
与异步上下文:
wrap- 在离开Reatom框架的异步边界(如fetch、定时器、DOM promise等)处,使用。在包装后的await之后,后续代码会回到上下文环境中——可直接调用atoms/actions。
await wrap(promise) - 仅当被传递给外部调用者(DOM监听器、定时器、第三方回调)时才使用
fn。它返回一个装饰后的函数;不会直接调用wrap(fn)。标记未赋值/传递给外部回调的裸fn。wrap(() => atom.set(...)) - 标记在actions/effects/async computeds内部无意义的——立即执行的包装IIFE没有任何作用;直接在Reatom框架内调用
wrap(() => atom.set(...))()即可。atom.set(...) - 标记;优先包装整个promise链或每个await步骤。
await wrap(fetch(url)).then(...) - 标记未使用或
wrap就调用Reatom状态的onEvent回调、DOM回调、定时器、.then(...)和外部事件监听器。requestAnimationFrame - 检查每个之后的代码:如果后续代码从异步续调用atoms/actions,通常应包装被await的promise。
await - 当等待DOM或外部事件时,优先使用而非原始事件监听器。
await wrap(onEvent(...)) - 应在感知Reatom的actions/effects/computeds/callbacks中使用,而非在普通可复用API助手内部。
wrap() - 降级处理的async/await可能会破坏上下文传播。当出现严格的上下文错误时,标记将异步代码转换为链的构建/测试目标。
.then() - 不要要求包装传递给Reatom钩子(如)的回调;钩子已在Reatom上下文中运行。
withCallHook
- 在离开Reatom框架的异步边界(如fetch、定时器、DOM promise等)处,使用
-
状态建模:
- 写入操作需通过完成。给响应式atom传递参数调用(如
.set(...))会抛出错误;调用counter(5)传递参数也会抛出错误。标记任何位置调用式的写入操作;读取操作是无参数调用。computed - Action状态(、
getCalls返回的列表)是临时的——会在下一个清理队列tick中被清除。标记将action的调用列表存储或后续读取为持久状态的代码;应将负载持久化到atoms中。action() - 集合原语(、
reatomMap、reatomSet、reatomArray、reatomRecord)通过其actions/不可变方法更新。标记对其状态进行原地变更的代码(如reatomLinkedList、map().set(...)):这会跳过失效检查并破坏相等性验证。array().push(...) - 无法替换atom引用,也无法覆盖现有键——冲突的方法名会在运行时抛出错误。标记扩展中覆盖
.extend(...)、set、subscribe或早期扩展方法的键。extend - 动态对象内部的可变字段应被atom化。
- 当项级本地atom更清晰时,标记并行UI状态,如单独的、
selectedIds或编辑映射。checkedIds - Action与纯转换的区别:
- 仅将一种数据形状映射为另一种的函数——无IO操作、无、无其他action调用——不是action。使用普通函数或
atom.set。computed - 执行副作用(网络、存储、定时器、DOM、日志)或更改Reatom状态(、调用actions)的函数是action,且应命名。
atom.set
- 仅将一种数据形状映射为另一种的函数——无IO操作、无
- 标记薄computed包装器:中的
computed(() => helper(model))仅读取model上的atom getters并返回派生值。该助手复制了computed的功能,但未在Reatom之外增加复用性。将派生逻辑放在computed体内,或通过父级的helper/withComputed附加——不要拆分为普通助手加透传computed。.extend - 标记接受带有atom的模型并调用/atom getters进行响应式派生的普通助手。它们隐藏了响应式图,容易产生薄computed包装器,且在computed/action框架外调用时不安全。
.data() - 直接使用适用于本地/简单更新。标记仅将值转发给atoms的“标识”actions。
atom.set - 带有副作用和多步骤流程的复杂转换应使用命名的actions。
- 相关状态和actions应通过或
.extend(...)分组在父级上,而非分散为同级导出。withActions(...) - 遵循核心模式,如和
reatomBoolean:创建父atom,然后通过reatomRoute附加相关方法、子computeds、加载器、路由工厂和助手。.extend - 对于作用域状态,考虑computed工厂模式:读取作用域键并返回该作用域的atoms/actions/forms,因此更改键会替换内部图。
computed - Computed工厂是通用的,不仅限于路由。在所选实体、编辑会话、模态框、标签页以及任何具有自身状态生命周期的命名工作单元中寻找它们。
- 有意选择工厂契约:当非活动作用域是普通状态时返回,或当读取命名作用域外的内容是bug时抛出错误。
null
- 写入操作需通过
-
命名与可追踪性:
- Atoms、computed值、effects和actions应命名。
- Atom/action/model工厂必须使用前缀(例如
reatom*、reatomFolderTreeNodeUi、reatomGalleryImage),而非reatomUser、get*或其他通用动词。标记分配命名atoms、computeds、effects或actions但读起来像普通getter或构造函数的工厂函数。create* - 嵌套/动态名称应保留结构,例如、
users.page或users#${id}.name。${target.name}.ready - 对热路径或嘈杂的相关状态和actions的跟踪名称,在段前添加前缀:与指针移动、滚动、调整大小或动画tick相关的atoms、computed、effects和actions。示例:
_、lightbox._panMove、lightbox._controlsActivity、lightbox._hideControlsAfterInactivity。保持日志可读性。imageGrid._width - 标记共享模型和示例中的匿名原语。
-
Effects、钩子与订阅:
- 使用处理派生状态,使用
computed处理副作用。effect - 是惰性的;检查预期加载的数据是否有订阅者或明确的路由/渲染路径。
computed - 未订阅的computeds在每次读取时都会重新验证;已订阅的computeds是推送缓存的。标记读取繁重的未订阅computeds的热循环,以及热路径上没有订阅者的昂贵computeds——考虑使用或订阅。
withMemo - 订阅者回调和队列effects会异步刷新(微任务)。标记在后同步断言订阅者已触发的代码(尤其是测试);等待微任务/
.set(...)或直接读取atom。sleep(0) - 共享默认全局上下文的测试必须使用(或
context.reset()作用域)隔离状态。标记atoms在测试用例之间泄漏状态的测试套件。context.start - 不是惰性的:它在创建时自动订阅,因此模块级的
effect会在导入时立即连接,并一直运行直到调用effect(...)。.unsubscribe() - 标记为“将状态放入模型”而提升到模块作用域的组件/功能作用域effects:它们失去了挂载/可见性作用域,在功能关闭后仍会继续运行(可能会在定时器上循环)。
- 优先在功能边界启动功能作用域effects,顺序如下:
- 路由加载器/路由初始化(最佳):加载器或路由所属的初始化action在功能作用域打开时创建effect;在路由卸载或作用域更改时中止/断开连接。
render - 功能模型上显式命名的/
initaction:在功能打开时调用一次(如灯箱打开、面板挂载、会话开始)。start - 组件挂载/清理(可接受但架构较弱):在挂载作用域中创建effect,并在销毁时调用
ref。.unsubscribe()
- 路由加载器/路由
- +
withConnectHook是一种良好模式,仅当effect不直接或间接读取钩子目标atom时适用。如果effect依赖于拥有connect钩子的同一atom,可能会创建无限连接/订阅循环。标记effect及类似代码。target.extend(withConnectHook(() => effect(() => target()))) - 当是合适的工具时,将其附加到effect不得依赖的作用域锚点(例如,对于读取
withConnectHook的幻灯片定时器,使用slideshowPlaying而非lightboxOpen上的slideshowPlaying)。withConnectHook - 标记对已存在的模块级调用
effect/.subscribe()的组件:effect在创建时已自动订阅,因此组件的订阅是冗余的,原始的自动订阅会泄漏(effect在卸载时永远不会断开连接)。.unsubscribe() - 优先使用将外部推送源(
reatomObservable、ResizeObserver、IntersectionObserver、套接字)桥接到连接驱动的atom,而非使用matchMedia连接观察者并写入同级结果atom。effect - 使用处理不依赖钩子目标的惰性外部订阅/轮询;验证清理和中止行为。
withConnectHook - 不要使用同步atoms与其他atoms;优先使用
withChangeHook或computed。withComputed
- 使用
-
路由:
- 优先使用加载器处理路由数据;加载器是带有
reatomRoute的异步computeds。withAsyncData - 路由加载器是computed工厂的典型代表:路由匹配/参数命名作用域,当作用域更改时,加载器创建的模型会被替换。
- 路由加载器/路由所属的初始化也是启动功能作用域effects(定时器、轮询、会话连接)的首选位置。避免模块级effects和在effect读取的atom上使用。
withConnectHook - 标记手动检查并返回
route.match()的组件;优先使用路由null选项和布局/出口。render - 当类型重要时,使用模式验证URL参数/搜索内容,并转换字符串参数而非假设为数字。
- 确认路由导航使用,链接在预期SPA拦截时使用
.go(...)。.path(...) - 路由路径无前导;
/接受参数,而非路径字符串。route.go() - 加载器接受一个合并的参数/搜索对象。是错误的写法。
(params, search) - 是路由选项;构造后
render是computed输出,而非可赋值的回调。route.render - 在内部创建并传递给UI的回调仍需
route.render(self)。wrap(...) - 认证、重定向和功能网关应放在/父级守卫中,而非可空的加载器负载中。
params() - 守卫或URL钩子中的重定向必须是幂等的,并在调用之前证明URL所有权。
.go(..., true) - 对于布局下的索引路由,优先使用获取活动状态;
exact()对子路由仍为true。match()
- 优先使用
-
中止、采样与并发:
- 和
take(...)返回promises;在异步actions/effects内部应使用onEvent(...)。await wrap(...) - 期望来自
race(...)的受控promises,而非普通promises。abortVar.createAndRun - 可中止Reatom上下文中的请求应传递。
signal: abortVar.require().signal - 对于返回带有异步actions/effects/轮询的同步模型的computed工厂,期望外部工厂使用;带有
withAbort()的异步computeds/加载器已支持中止。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 });单独的原始提交action可能会绕过验证。form.submit()
- 优先使用
-
持久化与URL同步:
- 优先使用Reatom助手,如、
withLocalStorage、withSessionStorage或特定存储的持久化扩展,而非临时effects。withSearchParams - 检查URL状态和持久化状态的解析/序列化行为,尤其是默认值和无效输入。
- 持久化键必须每个atom唯一;标记跨模型重复的键以及未升级+迁移的形状变更。
version - 对于缓存查询,检查选项是否符合意图:
withCache语义、swr/长度限制,以及staleTime默认值(仅空参数时为true)。标记非幂等actions上的缓存。ignoreAbort
- 优先使用Reatom助手,如
-
迁移正确性:
- 标记v3时代的过时API:、
ctx.schedule、ctx.spy、ctx.get、reatomAsync、reatomResource、reaction、atom.onChange、onConnect和withConcurrency。onCtxAbort - 优先使用当前等效项:、直接atom读取、
wrap、peek、action(...).extend(withAsync())、computed(...).extend(withAsyncData())、effect、withChangeHook、withConnectHook和withAbort。abortVar.subscribe
- 文档与示例:
- 文档不得将反模式作为推荐代码呈现。
- 如果展示错误代码,应清晰标记并立即提供推荐的Reatom版本。
- 检查导入是否与示例匹配,代码片段中使用的标识符是否存在,以及叙述声明是否与代码一致。
- 标记标题、正文、代码和API行为之间的不匹配。
- 示例应避免不安全的类型转换、匿名atoms/actions以及隐藏重要Reatom模式的虚假API。
- React与适配器:
- 调用atom getters的组件应为;
reatomComponent的结果是普通值,而非可调用的getters。useAtom - 读取/写入atoms或调用actions的手写UI回调需要,包括第三方控件回调。
wrap(...) - 不要在普通函数组件的JSX中直接调用;在Reatom框架内创建包装后的回调或向下传递。
wrap(...) - 将atoms作为props传递是有效的Reatom解耦方式。不要基于Redux直觉拒绝这种做法。
- DOM 回调、观察者通知和其他非Reatom入口点写入atoms时需要Reatom框架:
ref、context.start(() => ...)或wrap(...)。标记来自onEvent(...)或ref回调的裸ResizeObserver。atom.set
- 浏览器资源所有权:
- 将的结果视为生成它的异步computed(或connect钩子)所拥有的资源;当该computed重新运行/中止或模型断开连接时,必须撤销该URL。标记存储为普通字符串且无撤销路径的对象URL。
URL.createObjectURL - 将/
createImageBitmap、ImageBitmap.close()、OffscreenCanvas(及worker池)、Worker/ResizeObserver和IntersectionObserver监听器与Reatom生命周期绑定(异步computed中止、matchMedia/withConnectHook清理、withDisconnectHook或abortVar.subscribe)。onEvent - 标记手动实现的/
dispose()方法,而调用者必须记住调用这些方法,实际上cleanup()/withConnectHook/异步computed中止已能表达所有权。withDisconnectHook - 共享worker/解码器池应为命名服务模型,具有明确的连接/断开逻辑,以便文件夹/会话/路由重置终止陈旧工作而非泄漏。
- 模块结构与循环依赖:
- 标记在action/effect内部使用或
await import('./peer')仅为了打破循环依赖的情况。动态require('./peer')用于代码分割,而非打破循环;它还会将同步流程转为异步,并隐藏依赖关系以避免追踪。import() - 真正的修复是重构:将共享协调逻辑移到更高层级的编排模块,反转依赖关系,或传入依赖项。必须触及多个同级模块的重置/销毁逻辑通常属于静态导入它们的编排action。
Document Mismatch Checks
文档不匹配检查
When reviewing docs, tutorials, READMEs, generated summaries, or examples, actively search for these mismatches:
- Claim says "query/resource/data loading", but code uses , component lifecycle, refs, or manual status atoms instead of
effect.computed(...).extend(withAsyncData()) - Claim says "mutation/command", but code uses async for non-idempotent writes instead of
computed.action(...).extend(withAsync()) - Claim says "abort-aware" or "race-safe", but code lacks ,
withAbort, route loader behavior, orwithAsyncDataaround awaited work.wrap - Claim says sync Reatom writes are wrapped, but code uses without calling/passing the returned function.
wrap(() => atom.set(...)) - Claim says code preserves context with , but it uses
wrapinside an action/effect/computed where a directwrap(() => atom.set(...))()already runs in frame.atom.set(...) - Claim says "abortable fetch", but code does not pass to
signal: abortVar.require().signal.fetch - Claim uses , but the action/computed was not extended with
.status().{ status: true } - Claim says "route loader", but data fetching is placed in a rendered component or guarded with .
route.match() - Claim shows route loader params, but the code uses instead of one merged object.
(params, search) - Claim says route redirect/auth gate, but the code returns nullable loader data instead of blocking in / parent guards.
params() - Claim says "current Reatom", but snippet uses legacy ,
ctx,reatomResource,reatomAsync, orreaction.onConnect - Claim describes a model with relative state/actions, but snippets export separate sibling atoms/actions instead of grouping them through .
.extend - Claim shows an atom/action factory, but the function is named /
get*instead ofcreate*.reatom* - Claim shows a pure mapper/formatter/normalizer wrapped in , but the function has no IO and does not write state.
action(...) - Claim shows derived state as with a plain
computed(() => resolveX(model))helper that only reads atom getters — split indirection with no non-Reatom reuse.resolveX - Claim says form submit validation, but the code bypasses with a separate raw submit action.
form.submit() - Heading/prose says one atom/action name while the snippet uses another.
- Snippet omits essential imports such as ,
wrap,computed,withAsyncData,action, orwithAsync.reatomRoute - Example uses route/search params as typed numbers without schema transform/coercion.
- Example shows bad code without a "bad/problem" label and a corrected version nearby.
- Claim says an effect is "scoped to the screen/feature", but it is a module-level that self-subscribes at import and is only nominally re-subscribed from a component.
effect(...) - Claim says "moved state into the model", but the move turned a mount-scoped effect into an eager module-level effect with different lifetime semantics.
- Claim says an effect is "connect-hook scoped", but the effect reads the hook target atom (directly or indirectly), which can cause an infinite connect/subscribe loop.
- Claim says "no leaks / cleaned up on unmount", but object URLs, bitmaps, workers, or observers rely on a manual instead of connect-hook/abort ownership.
dispose() - Claim says modules are decoupled, but cycles are hidden behind /
await import()inside actions.require()
在审查文档、教程、README、生成的摘要或示例时,主动寻找以下不匹配情况:
- 声明称“查询/资源/数据加载”,但代码使用、组件生命周期、refs或手动状态atom而非
effect。computed(...).extend(withAsyncData()) - 声明称“变更/命令”,但代码使用异步处理非幂等写入而非
computed。action(...).extend(withAsync()) - 声明称“支持中止”或“竞争安全”,但代码缺少、
withAbort、路由加载器行为或在await工作周围使用withAsyncData。wrap - 声明称同步Reatom写入已被包装,但代码使用却未调用/传递返回的函数。
wrap(() => atom.set(...)) - 声明称代码使用保留上下文,但在action/effect/computed内部使用
wrap,而直接调用wrap(() => atom.set(...))()已在框架内运行。atom.set(...) - 声明称“可中止的请求”,但代码未将传递给
signal: abortVar.require().signal。fetch - 声明使用,但action/computed未使用
.status()扩展。{ status: true } - 声明称“路由加载器”,但数据请求放在已渲染的组件中或使用保护。
route.match() - 声明展示路由加载器参数,但代码使用而非一个合并对象。
(params, search) - 声明称路由重定向/认证网关,但代码返回可空的加载器数据而非在/父级守卫中阻止。
params() - 声明称“当前Reatom”,但代码片段使用旧版、
ctx、reatomResource、reatomAsync或reaction。onConnect - 声明描述带有相关状态/actions的模型,但代码片段导出单独的同级atoms/actions而非通过分组。
.extend - 声明展示atom/action工厂,但函数命名为/
get*而非create*。reatom* - 声明展示纯映射器/格式化器/标准化器包装在中,但该函数无IO操作且不写入状态。
action(...) - 声明展示派生状态为,其中普通
computed(() => resolveX(model))助手仅读取atom getters——拆分了间接层但无Reatom之外的复用性。resolveX - 声明称表单提交验证,但代码绕过使用单独的原始提交action。
form.submit() - 标题/正文使用一个atom/action名称,而代码片段使用另一个。
- 代码片段省略必要的导入,如、
wrap、computed、withAsyncData、action或withAsync。reatomRoute - 示例将路由/搜索参数作为类型化数字使用,而未进行模式转换/强制类型转换。
- 示例展示错误代码但未标记“错误/问题”标签,且附近未提供修正版本。
- 声明称effect“作用域到屏幕/功能”,但它是模块级,在导入时自动订阅,仅从组件名义上重新订阅。
effect(...) - 声明称“将状态移到模型中”,但移动将挂载作用域的effect转为了具有不同生命周期语义的急切模块级effect。
- 声明称effect“作用域到connect钩子”,但effect读取钩子目标atom(直接或间接),这可能导致无限连接/订阅循环。
- 声明称“无泄漏/在卸载时清理”,但对象URL、位图、workers或观察者依赖手动而非connect钩子/中止所有权。
dispose() - 声明称模块已解耦,但循环依赖隐藏在action内部的/
await import()之后。require()
Typical Mismatches And Fixes
典型不匹配与修复方案
Query Implemented As Imperative Effect
查询实现为命令式Effect
Problem:
ts
const users = atom<User[]>([], 'users')
effect(async () => {
users.set(await api.getUsers(page()))
}, 'users.fetch')Fix:
ts
const users = computed(async () => {
return await wrap(api.getUsers(page()))
}, 'users').extend(withAsyncData({ initState: [] }))Why: query data should be lazy, abort-aware, and expose , , , , , and .
datareadyerrorstatusretryreset问题:
ts
const users = atom<User[]>([], 'users')
effect(async () => {
users.set(await api.getUsers(page()))
}, 'users.fetch')修复:
ts
const users = computed(async () => {
return await wrap(api.getUsers(page()))
}, 'users').extend(withAsyncData({ initState: [] }))原因:查询数据应是惰性的、支持中止的,并暴露、、、、和。
datareadyerrorstatusretryresetwrap
Chained Incorrectly
wrapwrap
链式调用错误
wrapProblem:
ts
const response = await wrap(fetch(url)).then((res) => res.json())
data.set(response)Fix:
ts
const response = await wrap(fetch(url))
const payload: Payload = await wrap(response.json())
data.set(payload)Why: each async boundary is visible to Reatom and preserves tracing/cancellation.
问题:
ts
const response = await wrap(fetch(url)).then((res) => res.json())
data.set(response)修复:
ts
const response = await wrap(fetch(url))
const payload: Payload = await wrap(response.json())
data.set(payload)原因:每个异步边界对Reatom可见,并保留追踪/取消机制。
wrap
Used As A Statement (Function Not Called)
wrapwrap
用作语句(函数未调用)
wrapProblem:
ts
} finally {
wrap(() => activeRequests.set((count) => count - 1))
}Fix:
ts
} finally {
activeRequests.set((count) => count - 1)
}Alternative fix when the callback is passed to an external API:
ts
button.addEventListener(
'click',
wrap(() => counter.set((value) => value + 1)),
)Why: decorates for external callers; it does not execute . Inside an action/effect/async computed — including after — context is already restored; call atoms directly.
wrap(fn)fnfnfinallyawait wrap(...)问题:
ts
} finally {
wrap(() => activeRequests.set((count) => count - 1))
}修复:
ts
} finally {
activeRequests.set((count) => count - 1)
}当回调传递给外部API时的替代修复:
ts
button.addEventListener(
'click',
wrap(() => counter.set((value) => value + 1)),
)原因:为外部调用者装饰;不会执行。在action/effect/async computed内部——包括之后的块——上下文已恢复;直接调用atoms即可。
wrap(fn)fnfnawait wrap(...)finallyPointless wrap
IIFE
wrap无意义的wrap
IIFE
wrapProblem:
ts
} finally {
wrap(() => activeRequests.set((count) => count - 1))()
}Fix:
ts
} finally {
activeRequests.set((count) => count - 1)
}Why: inside a Reatom frame is just an indirect call. Reserve for callbacks handed to DOM/timers/third-party code; reserve for async boundaries.
wrap(() => ...)()wrap(fn)await wrap(promise)问题:
ts
} finally {
wrap(() => activeRequests.set((count) => count - 1))()
}修复:
ts
} finally {
activeRequests.set((count) => count - 1)
}原因:在Reatom框架内的只是间接调用。仅在将回调交给DOM/定时器/第三方代码时使用;仅在异步边界处使用。
wrap(() => ...)()wrap(fn)await wrap(promise)wrap
Missing After Async Boundary
wrap异步边界后缺少wrap
wrapProblem:
ts
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')Fix:
ts
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')Why: the state update runs after async work, so the async boundary must preserve Reatom context.
问题:
ts
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')修复:
ts
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上下文。
Callback Calls Reatom Without Context
回调无上下文调用Reatom
Problem:
ts
addEventListener('online', () => {
online.set(true)
})Fix:
ts
onEvent(globalThis, 'online', () => {
online.set(true)
})Alternative fix when a raw callback API must be used:
ts
addEventListener(
'online',
wrap(() => {
online.set(true)
}),
)Why: callbacks are async entry points too. Preserve context or use Reatom's abort-aware event helper.
问题:
ts
addEventListener('online', () => {
online.set(true)
})修复:
ts
onEvent(globalThis, 'online', () => {
online.set(true)
})必须使用原始回调API时的替代修复:
ts
addEventListener(
'online',
wrap(() => {
online.set(true)
}),
)原因:回调也是异步入口点。保留上下文或使用Reatom的支持中止的事件助手。
Awaited Event Not Wrapped
等待的事件未被包装
Problem:
ts
const confirm = action(async (button: HTMLButtonElement) => {
await onEvent(button, 'click')
confirmed.set(true)
}, 'confirm')Fix:
ts
const confirm = action(async (button: HTMLButtonElement) => {
await wrap(onEvent(button, 'click'))
confirmed.set(true)
}, 'confirm')Why: returns a promise. Await it through inside async actions/effects.
onEvent(...)wrap问题:
ts
const confirm = action(async (button: HTMLButtonElement) => {
await onEvent(button, 'click')
confirmed.set(true)
}, 'confirm')修复:
ts
const confirm = action(async (button: HTMLButtonElement) => {
await wrap(onEvent(button, 'click'))
confirmed.set(true)
}, 'confirm')原因:返回promise。在异步actions/effects内部通过等待它。
onEvent(...)wrapAbortable Fetch Without Abort Signal
可中止请求无中止信号
Problem:
ts
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())Fix:
ts
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())Why: , route loaders, , and abort-aware effects can cancel the Reatom frame; fetch should receive the same abort signal.
withAsyncDatawithAbort问题:
ts
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())修复:
ts
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())原因:、路由加载器、和支持中止的effects可以取消Reatom框架;请求应接收相同的中止信号。
withAsyncDatawithAbortStatus Used Without Enabling It
未启用状态就使用
Problem:
ts
const submit = action(async () => {
await wrap(api.save(form()))
}, 'form.submit').extend(withAsync())
const status = submit.status()Fix:
ts
const submit = action(async () => {
await wrap(api.save(form()))
}, 'form.submit').extend(withAsync({ status: true }))
const status = submit.status()Alternative fix:
ts
const ready = submit.ready()
const pending = submit.pending()
const error = submit.error()Why: is disabled by default for async extensions. Use only when the full status model is needed.
status{ status: true }问题:
ts
const submit = action(async () => {
await wrap(api.save(form()))
}, 'form.submit').extend(withAsync())
const status = submit.status()修复:
ts
const submit = action(async () => {
await wrap(api.save(form()))
}, 'form.submit').extend(withAsync({ status: true }))
const status = submit.status()替代修复:
ts
const ready = submit.ready()
const pending = submit.pending()
const error = submit.error()原因:异步扩展默认禁用。仅当需要完整状态模型时才使用。
status{ status: true }Identity Action
标识Action
Problem:
ts
const query = atom('', 'search.query')
const setQuery = action((next: string) => query.set(next), 'search.query.set')Fix:
ts
const query = atom('', 'search.query')
query.set('next value')Why: simple local updates do not need forwarding actions. Use actions for side effects and state-changing flows, not for pure data mapping.
问题:
ts
const query = atom('', 'search.query')
const setQuery = action((next: string) => query.set(next), 'search.query.set')修复:
ts
const query = atom('', 'search.query')
query.set('next value')原因:简单的本地更新不需要转发actions。仅在处理副作用和状态变更流程时使用actions,而非纯数据映射。
Thin Computed Wrapper Over Plain Helper
普通助手之上的薄Computed包装器
Problem:
ts
export function resolveDownloadUrl(image: ReatomImage): string {
return image.fullImageUrl.data() ?? image.thumbnail.data()?.url ?? ''
}
const downloadUrl = computed(
() => resolveDownloadUrl(imageModel),
`${name}.display.downloadUrl`,
)Fix:
ts
const downloadUrl = computed(
() =>
imageModel.fullImageUrl.data() ?? imageModel.thumbnail.data()?.url ?? '',
`${name}.display.downloadUrl`,
)Alternative fix when the same derivation is reused in tests or non-Reatom code: keep a pure function on plain data (URLs, DTOs), not on atom-bearing models; let the computed map atoms to that shape.
Why: a computed that only delegates to a helper reading atoms adds indirection without traceability benefit. The computed body (or on the parent) is the derivation; plain helpers belong on plain values, not as a shadow layer over atoms.
withComputed问题:
ts
export function resolveDownloadUrl(image: ReatomImage): string {
return image.fullImageUrl.data() ?? image.thumbnail.data()?.url ?? ''
}
const downloadUrl = computed(
() => resolveDownloadUrl(imageModel),
`${name}.display.downloadUrl`,
)修复:
ts
const downloadUrl = computed(
() =>
imageModel.fullImageUrl.data() ?? imageModel.thumbnail.data()?.url ?? '',
`${name}.display.downloadUrl`,
)当相同派生逻辑在测试或非Reatom代码中复用时的替代修复:保留针对纯数据(URL、DTO)的纯函数,而非针对带有atom的模型;让computed将atoms映射为该形状。
原因:仅委托给读取atoms的助手的computed增加了间接层,但没有可追踪性优势。computed体(或父级上的)是派生逻辑;普通助手应针对纯值,而非作为atoms之上的影子层。
withComputedAtom Factory Named Like A Getter
Atom工厂命名为Getter风格
Problem:
ts
export const getFolderTreeNodeUi = (folderPath: string) => ({
expanded: reatomBoolean(false, `folderTree.${folderPath}.expanded`),
isSelected: computed(
() => currentFolder()?.path === folderPath,
`folderTree.${folderPath}.isSelected`,
),
})Fix:
ts
export const reatomFolderTreeNodeUi = (folderPath: string) => ({
expanded: reatomBoolean(false, `folderTree.${folderPath}.expanded`),
isSelected: computed(
() => currentFolder()?.path === folderPath,
`folderTree.${folderPath}.isSelected`,
),
})Why: Reatom factories create traced atoms and actions; signals that contract and matches core helpers like , , and . Plain / names hide lifecycle and naming rules for nested units.
reatom*reatomBooleanreatomRoutereatomFormget*create*问题:
ts
export const getFolderTreeNodeUi = (folderPath: string) => ({
expanded: reatomBoolean(false, `folderTree.${folderPath}.expanded`),
isSelected: computed(
() => currentFolder()?.path === folderPath,
`folderTree.${folderPath}.isSelected`,
),
})修复:
ts
export const reatomFolderTreeNodeUi = (folderPath: string) => ({
expanded: reatomBoolean(false, `folderTree.${folderPath}.expanded`),
isSelected: computed(
() => currentFolder()?.path === folderPath,
`folderTree.${folderPath}.isSelected`,
),
})原因:Reatom工厂创建可追踪的atoms和actions;前缀表明了这种契约,并与核心助手如、和保持一致。普通的/名称隐藏了嵌套单元的生命周期和命名规则。
reatom*reatomBooleanreatomRoutereatomFormget*create*Relative State Scattered As Sibling Exports
相关状态分散为同级导出
Problem:
ts
export const search = atom('', 'search')
export const searchIsEmpty = computed(
() => search().trim() === '',
'search.isEmpty',
)
export const clearSearch = action(() => search.set(''), 'search.clear')Fix:
ts
export const search = atom('', 'search').extend((target) => ({
isEmpty: computed(() => target().trim() === '', `${target.name}.isEmpty`),
clear: action(() => target.set(''), `${target.name}.clear`),
}))Why: relative states and actions should live on the parent model, like groups boolean actions and attaches , , , and child route helpers.
reatomBooleanreatomRoutegoloaderrender问题:
ts
export const search = atom('', 'search')
export const searchIsEmpty = computed(
() => search().trim() === '',
'search.isEmpty',
)
export const clearSearch = action(() => search.set(''), 'search.clear')修复:
ts
export const search = atom('', 'search').extend((target) => ({
isEmpty: computed(() => target().trim() === '', `${target.name}.isEmpty`),
clear: action(() => target.set(''), `${target.name}.clear`),
}))原因:相关状态和actions应存在于父模型上,就像分组布尔actions,附加、、和子路由助手一样。
reatomBooleanreatomRoutegoloaderrenderParallel UI State Instead Of Atomization
并行UI状态而非Atom化
Problem:
ts
const users = atom<UserDto[]>([], 'users')
const selectedUserIds = atom<Set<string>>(new Set(), 'users.selectedIds')Fix:
ts
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`),
})),
)
},
}))Why: mutable per-item state belongs near the item to avoid parallel structures and broad list updates.
问题:
ts
const users = atom<UserDto[]>([], 'users')
const selectedUserIds = atom<Set<string>>(new Set(), 'users.selectedIds')修复:
ts
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`),
})),
)
},
}))原因:每个项的可变状态应靠近该项,以避免并行结构和广泛的列表更新。
Manual Route Rendering
手动路由渲染
Problem:
tsx
export function UsersPage() {
if (!usersRoute.match()) return null
return <Users />
}Fix:
ts
export const usersRoute = layoutRoute.reatomRoute({
path: 'users',
render() {
return <Users />
},
})Why: route handles mounting, exact matching, loaders, layouts, and outlets.
render问题:
tsx
export function UsersPage() {
if (!usersRoute.match()) return null
return <Users />
}修复:
ts
export const usersRoute = layoutRoute.reatomRoute({
path: 'users',
render() {
return <Users />
},
})原因:路由处理挂载、精确匹配、加载器、布局和出口。
renderComponent-Scoped Effect Lifted To Module Scope (Eager Forever)
组件作用域Effect提升到模块作用域(永久急切运行)
Problem:
ts
// 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
}}Fix (preferred — route/feature init):
ts
// 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')Alternative fix (acceptable — mount in the feature component):
tsx
// components/Slideshow.tsx
ref={() => {
const {unsubscribe} = effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
return unsubscribe
}}Why: self-subscribes at creation, so a module-level effect is connected eagerly and never disconnects; the component's extra is redundant and the original self-subscription leaks. Start the effect at the feature boundary (route loader, explicit init action, or component mount), not as a forever-connected module singleton.
effect(...).subscribe()问题:
ts
// 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
}}修复(首选——路由/功能初始化):
ts
// 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')替代修复(可接受——在功能组件中挂载):
tsx
// components/Slideshow.tsx
ref={() => {
const {unsubscribe} = effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
return unsubscribe
}}原因:在创建时自动订阅,因此模块级effect会立即连接且永远不会断开;组件额外的是冗余的,原始的自动订阅会泄漏。在功能边界(路由加载器、显式初始化action或组件挂载)启动effect,而非作为永久连接的模块单例。
effect(...).subscribe()Effect Inside withConnectHook On Its Own Dependency (Infinite Loop)
withConnectHook内部的Effect依赖自身(无限循环)
Problem:
ts
export const slideshowPlaying = reatomBoolean(false, 'slideshowPlaying').extend(
withConnectHook(() => {
effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}),
)Fix (scope anchor the effect does not read):
ts
export const lightboxOpen = reatomBoolean(false, 'lightboxOpen').extend(
withConnectHook(() => {
effect(async () => {
while (peek(slideshowPlaying)) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}),
)Better fix (explicit init at feature open):
ts
export const startSlideshowSession = action(() => {
effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}, 'slideshow.startSession')Why: a connect hook runs when its target gets subscribers. If the nested effect reads that same target (directly or through a computed), connect/subscribe can feed back forever. Either attach the hook to a different scope anchor, use for gate checks only, or start the effect from route loader / init action / component mount instead.
peek问题:
ts
export const slideshowPlaying = reatomBoolean(false, 'slideshowPlaying').extend(
withConnectHook(() => {
effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}),
)修复(使用Effect不读取的作用域锚点):
ts
export const lightboxOpen = reatomBoolean(false, 'lightboxOpen').extend(
withConnectHook(() => {
effect(async () => {
while (peek(slideshowPlaying)) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}),
)更好的修复(功能打开时显式初始化):
ts
export const startSlideshowSession = action(() => {
effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}, 'slideshow.startSession')原因:connect钩子在目标获得订阅者时运行。如果嵌套的effect读取同一个目标(直接或通过computed),连接/订阅可能会永远循环。要么将钩子附加到不同的作用域锚点,仅使用进行 gate 检查,要么从路由加载器/初始化action/组件挂载启动effect。
peekObject URL Leaked As A Plain String
对象URL作为普通字符串泄漏
Problem:
ts
const previewUrl = computed(() => {
const blob = imageBlob.data()
return blob ? URL.createObjectURL(blob) : ''
}, 'image.previewUrl')Fix:
ts
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: '' }))Why: allocates a resource. Tie revocation to the owning computed's abort/disconnect so the URL is freed when the model re-runs or disconnects, instead of leaking one URL per recomputation.
URL.createObjectURL问题:
ts
const previewUrl = computed(() => {
const blob = imageBlob.data()
return blob ? URL.createObjectURL(blob) : ''
}, 'image.previewUrl')修复:
ts
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: '' }))原因:分配了资源。将撤销与所属computed的中止/断开连接绑定,以便在模型重新运行或断开连接时释放URL,而非每次重新计算都泄漏一个URL。
URL.createObjectURLCompact Gotcha Fixes
常见问题快速修复
- Module-level -> eager at import; start it from route loader, explicit init action, or component mount — not as a forever-connected singleton.
effect(...) - +
withConnectHookthat reads the hook target -> infinite connect loop; use a different scope anchor,effectfor gates, or route/init/component mount instead.peek - /
await import('./peer')to break a cycle -> restructure modules or use an orchestration action; dynamic import is for code-splitting.require('./peer') - Hand-rolled for object URLs/bitmaps/observers/workers -> tie cleanup to
dispose()/withConnectHookor async-computed abort.withDisconnectHook - from a DOM
atom.set/observer callback -> passrefto the callback API, orwrap(() => atom.set(...)).context.start(() => atom.set(...)) - as a standalone statement -> dead code; call
wrap(() => atom.set(...))directly or passatom.set(...)externally.wrap(fn) - inside action/effect/computed -> pointless; call
wrap(() => atom.set(...))()directly.atom.set(...) - with plain
computed(() => resolveX(model))reading atoms -> inline in computed or attach viaresolveXon the parent.withComputed - to write ->
counter(5); positional-call writes on reactive atoms throw.counter.set(5) - without
action.retry()->cacheParams, or retry the computed instead.withAsync({ cacheParams: true }) - -> reorder:
.extend(withCache(), withAsync())/withAsyncfirst,withAsyncDataafter.withCache - Reading later as data -> action call lists are cleared next tick; store payloads in atoms.
getCalls(someAction) - /
reatomMap()().set(k, v)-> use the primitive's actions; in-place mutation skips invalidation.reatomArray()().push(x) - Synchronous assertion after expecting a subscriber to have fired -> await a microtask; notifications are queued.
.set(...) - Shared state between tests -> in
context.reset()or scope withbeforeEach.context.start - ->
status.isPending(); status flags are properties.status.isPending - ->
status.error; errors stay on the async target atom.target.error() - ->
async loader(params, search); loaders receive one merged params/search object.async loader({ q, userId }) - Nullable auth loader -> ; guards should block route ownership before loaders run.
params() { return allowed ? {} : null } - ->
route.go('/users'); route paths are declared without leadingusersRoute.go(params)./ - Module-level route form singleton -> route/scoped factory or loader-created form when lifetime follows the route.
- 模块级-> 导入时急切运行;从路由加载器、显式初始化action或组件挂载启动——而非作为永久连接的单例。
effect(...) - + 读取钩子目标的effect -> 无限连接循环;使用不同的作用域锚点、
withConnectHook进行gate检查,或路由/初始化/组件挂载替代。peek - /
await import('./peer')打破循环依赖 -> 重构模块或使用编排action;动态导入用于代码分割。require('./peer') - 对象URL/位图/观察者/workers的手动-> 将清理与
dispose()/withConnectHook或异步computed中止绑定。withDisconnectHook - DOM /观察者回调中的
ref-> 将atom.set传递给回调API,或使用wrap(() => atom.set(...))。context.start(() => atom.set(...)) - 独立语句的-> 死代码;直接调用
wrap(() => atom.set(...))或传递atom.set(...)给外部。wrap(fn) - action/effect/computed内部的-> 无意义;直接调用
wrap(() => atom.set(...))()。atom.set(...) - 普通读取atoms的
resolveX-> 内联到computed中或通过父级的computed(() => resolveX(model))附加。withComputed - 写入 ->
counter(5);响应式atom的位置调用写入会抛出错误。counter.set(5) - 无的
cacheParams->action.retry(),或重试computed。withAsync({ cacheParams: true }) - -> 重新排序:
.extend(withCache(), withAsync())/withAsync在前,withAsyncData在后。withCache - 稍后读取作为数据 -> action调用列表在下一个tick清除;将负载存储在atoms中。
getCalls(someAction) - /
reatomMap()().set(k, v)-> 使用原语的actions;原地变更跳过失效检查。reatomArray()().push(x) - 后同步断言订阅者已触发 -> 等待微任务;通知是排队的。
.set(...) - 测试之间共享状态 -> 在中使用
beforeEach或通过context.reset()作用域隔离。context.start - ->
status.isPending();状态标志是属性。status.isPending - ->
status.error;错误保留在异步目标atom上。target.error() - ->
async loader(params, search);加载器接收一个合并的参数/搜索对象。async loader({ q, userId }) - 可空认证加载器 -> ;守卫应在加载器运行前阻止路由所有权。
params() { return allowed ? {} : null } - ->
route.go('/users');路由路径声明时无前导usersRoute.go(params)。/ - 模块级路由表单单例 -> 路由/作用域工厂或加载器创建的表单,当生命周期跟随路由时。
Async Extensions Ordered After Cache
异步扩展在缓存之后
Problem:
ts
const users = computed(async () => {
return await wrap(api.getUsers())
}, 'users').extend(withCache(), withAsyncData())Fix:
ts
const users = computed(async () => {
return await wrap(api.getUsers())
}, 'users').extend(withAsyncData(), withCache())Why: / refuse to attach after (runtime ), and the async middleware must observe cache hits to keep , , and consistent.
withAsyncwithAsyncDatawithCacheReatomErrorpendingdatastatus问题:
ts
const users = computed(async () => {
return await wrap(api.getUsers())
}, 'users').extend(withCache(), withAsyncData())修复:
ts
const users = computed(async () => {
return await wrap(api.getUsers())
}, 'users').extend(withAsyncData(), withCache())原因:/拒绝在之后附加(运行时),且异步中间件必须观察缓存命中以保持、和一致。
withAsyncwithAsyncDatawithCacheReatomErrorpendingdatastatusAction Call List Treated As Durable State
Action调用列表视为持久状态
Problem:
ts
const addToast = action((toast: Toast) => toast, 'addToast')
const toasts = computed(
() => getCalls(addToast).map((call) => call.payload),
'toasts',
)Fix:
ts
const toasts = atom<Toast[]>([], 'toasts').extend(
withActions((target) => ({
add: (toast: Toast) => target.set((list) => [...list, toast]),
})),
)Why: action state is an autoclearable array, wiped in the next cleanup tick. It is for reacting to calls within a transaction, not for storage; durable data belongs in atoms.
问题:
ts
const addToast = action((toast: Toast) => toast, 'addToast')
const toasts = computed(
() => getCalls(addToast).map((call) => call.payload),
'toasts',
)修复:
ts
const toasts = atom<Toast[]>([], 'toasts').extend(
withActions((target) => ({
add: (toast: Toast) => target.set((list) => [...list, toast]),
})),
)原因:action状态是自动清除的数组,在下一个清理tick中被擦除。它用于在事务内响应调用,而非存储;持久数据应放在atoms中。
Stale v3 API
过时v3 API
Problem:
ts
const resource = reatomResource(async (ctx) => {
const response = await ctx.schedule(fetch('/api/users'))
return response.json()
}, 'users')Fix:
ts
const users = computed(async () => {
const response = await wrap(fetch('/api/users'))
return await wrap(response.json())
}, 'users').extend(withAsyncData())Why: current Reatom uses implicit context, , and async computed resources.
wrap问题:
ts
const resource = reatomResource(async (ctx) => {
const response = await ctx.schedule(fetch('/api/users'))
return response.json()
}, 'users')修复:
ts
const users = computed(async () => {
const response = await wrap(fetch('/api/users'))
return await wrap(response.json())
}, 'users').extend(withAsyncData())原因:当前Reatom使用隐式上下文、和异步computed资源。
wrapFinding Format
问题报告格式
Use this format for each issue:
md
- [Severity] `path-or-symbol`: Problem statement.
Why it matters: concrete Reatom rule or failure mode.
Fix: specific code-level change.Severity guide:
- Critical: incorrect state, lost async context, cancellation/race bug, broken route behavior, or stale API that cannot work.
- High: recommended Reatom model is bypassed in a way that risks leaks, eager fetches, stale data, or misleading docs.
- Medium: maintainability, traceability, naming, atomization, or tests are materially weaker.
- Low: style/docs clarity that could confuse future agents but is not likely to break behavior.
If no findings remain, say so directly and list residual risks, especially untested async cancellation, route loader behavior, or documentation examples not executed.
每个问题使用以下格式:
md
- [严重程度] `路径或符号`: 问题描述。
重要性:具体的Reatom规则或失败模式。
修复:具体的代码级变更。严重程度指南:
- 关键:状态错误、异步上下文丢失、取消/竞争bug、路由行为损坏,或无法工作的过时API。
- 高:绕过推荐的Reatom模型,存在泄漏风险、急切请求、陈旧数据或误导性文档。
- 中:可维护性、可追踪性、命名、atom化或测试明显较弱。
- 低:可能混淆未来Agent但不太可能破坏行为的风格/文档清晰度问题。
如果没有发现问题,请直接说明,并列出剩余风险,尤其是未测试的异步取消、路由加载器行为或未执行的文档示例。
Final Review Checklist
最终审查清单
- Did you inspect both code and docs changed by the agent?
- Did you compare claims against REFERENCE.md, not generic React/Solid/Vue habits?
- Did you check async context and after every await/callback boundary?
wrap - Did you flag bare statements and pointless
wrap(() => ...)IIFEs inside Reatom frames?wrap(() => ...)() - Did you challenge imperative fetching, manual routing, and parallel mutable state?
- Did you flag used for pure mappers with no IO or state writes?
action - Did you flag thin wrappers where the helper only reads atom getters?
computed(() => helper(model)) - Did you flag atom/action factories named /
get*instead ofcreate*?reatom* - Did you check effect lifetime: module-level eagerness, feature init (route loader / init action / component mount), connect-hook dependency traps, and redundant component subscriptions?
effect - Did you check that object URLs, bitmaps, workers, and observers are owned by Reatom lifecycle rather than manual ?
dispose() - Did you check ordering (async before cache),
.extend(...)/retryoption requirements, and key collisions in extensions?status - Did you check write syntax (vs positional call), in-place mutation of collection primitives, and reliance on ephemeral action call lists?
.set - Did you check timing assumptions: queued notifications, unsubscribed computed revalidation, and test context isolation via ?
context.reset() - Did you verify the examples are copyable and do not hide key imports?
- Did you avoid approving without at least considering tests or examples that exercise the Reatom behavior?
- 是否检查了Agent更改的代码和文档?
- 是否对照REFERENCE.md而非通用React/Solid/Vue习惯验证声明?
- 是否检查了每个await/回调边界后的异步上下文和?
wrap - 是否标记了Reatom框架内的裸语句和无意义的
wrap(() => ...)IIFEs?wrap(() => ...)() - 是否质疑命令式请求、手动路由和并行可变状态?
- 是否标记了用于无IO或状态写入的纯映射器的?
action - 是否标记了助手仅读取atom getters的薄包装器?
computed(() => helper(model)) - 是否标记了命名为/
get*而非create*的atom/action工厂?reatom* - 是否检查了effect生命周期:模块级effect的急切性、功能初始化(路由加载器/初始化action/组件挂载)、connect钩子依赖陷阱和冗余组件订阅?
- 是否检查了对象URL、位图、workers和观察者是否由Reatom生命周期而非手动管理?
dispose() - 是否检查了顺序(异步在缓存之前)、
.extend(...)/retry选项要求以及扩展中的键冲突?status - 是否检查了写入语法(vs 位置调用)、集合原语的原地变更以及对临时action调用列表的依赖?
.set - 是否检查了时序假设:排队通知、未订阅computed的重新验证以及通过隔离测试上下文?
context.reset() - 是否验证示例可复制且未隐藏关键导入?
- 是否在批准前至少考虑了测试或示例是否验证了Reatom行为?