Reatom Review
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.
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.
Review 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.
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.
- For mutations/commands, expect
action(async () => ...).extend(withAsync(...))
, plus or transactions when needed.
- is available only when / enables . Otherwise prefer , , and .
- on an action requires
withAsync({ cacheParams: true })
; without it throws at call time. Computeds can retry without options.
- Extension order matters: / must be applied before (attaching after throws). Review every chain for ordering, not just presence.
- Async helper atoms are getters: use , , , and . Do not render or destructure atom objects as inert values.
- Status flags are properties, for example , not functions. Use the target's atom; do not invent .
-
- 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.
- Use only when is passed to an external caller (DOM listener, timer, third-party callback). It returns a decorated function; it does not call . Flag bare
wrap(() => atom.set(...))
with no assignment/pass-through to an external callback.
- Flag pointless
wrap(() => atom.set(...))()
inside actions/effects/async computeds — an immediate wrapped IIFE adds nothing; just call in the Reatom frame.
- Flag
await wrap(fetch(url)).then(...)
; prefer wrapping the whole promise chain or wrapping each awaited step.
- Flag callbacks, DOM callbacks, timers, , and external event listeners that call Reatom state without or .
- Check after every : if later code calls atoms/actions from an async continuation, the awaited promise should usually be wrapped.
- Prefer over raw event listeners when awaiting DOM or external events.
- belongs in Reatom-aware actions/effects/computeds/callbacks, not inside plain reusable API helpers.
- Downleveled async/await can break context propagation. Flag build/test targets that transform async code to chains when strict context errors appear.
- Do not ask to wrap callbacks passed into Reatom hooks such as ; hooks already run in Reatom context.
-
State modeling:
- Writes go through . Calling a reactive atom with arguments () throws; calling a with arguments throws. Flag any positional-call writes; reads are zero-arg calls.
- Action state (, return 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.
- Collection primitives (, , , , ) update through their actions/immutable methods. Flag in-place mutation of their state (, ): it skips invalidation and corrupts equality checks.
- cannot replace the atom reference and cannot override existing keys — colliding method names throw at runtime. Flag extensions whose assigned keys shadow , , , or earlier extension methods.
- Mutable fields inside dynamic objects should be atomized.
- Flag normalized parallel UI state like separate , , or edit maps when item-local atoms would be clearer.
- 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 .
- A function that performs side effects (network, storage, timers, DOM, logging) or changes Reatom state (, calling actions) is an action and should be named.
- Flag thin computed wrappers: a
computed(() => helper(model))
where 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 with / on the parent — do not split into a plain helper plus a pass-through computed.
- 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.
- Direct is fine for local/simple updates. Flag "identity" actions that only forward values to atoms.
- 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 , not scattered as sibling exports.
- Follow core patterns like and : create the parent atom, then attach related methods, child computeds, loaders, route factories, and helpers through .
- 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 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.
-
Naming and traceability:
- Atoms, computed values, effects, and actions should be named.
- Atom/action/model factories must use the prefix (for example , , ), not , , or other generic verbs. Flag factory functions that allocate named atoms, computeds, effects, or actions but read like plain getters or constructors.
- Nested/dynamic names should preserve structure, for example , , or .
- 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._controlsActivity
, lightbox._hideControlsAfterInactivity
, . Keeps logs readable.
- Flag anonymous primitives in shared models and examples.
-
Effects, hooks, and subscriptions:
- Use for derived state and for side effects.
- is lazy; check that data expected to load has a subscriber or an explicit route/render path.
- 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.
- Subscriber callbacks and queue effects flush asynchronously (microtask). Flag code (especially tests) that asserts a subscriber fired synchronously after ; await a microtask/ or read the atom directly.
- Tests sharing the default global context must isolate state with (or scoping). Flag test suites where atoms leak state between cases.
- is NOT lazy: it self-subscribes at creation, so a module-level connects eagerly at import and runs forever until .
- 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.
- Explicit named / action on the feature model: called once when the feature opens (lightbox open, panel mount, session start).
- Component mount / cleanup (acceptable but weaker architecture): create the effect in the mounted scope and call on teardown.
- + is 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. Flag
target.extend(withConnectHook(() => effect(() => target())))
and similar.
- When is the right tool, attach it to a scope anchor the effect must not depend on (for example for a slideshow timer that reads , not on itself).
- Flag a component that calls / on an already module-level : the effect self-subscribed at creation, so the component subscription is redundant and the original self-subscription leaks (the effect never disconnects on unmount).
- Prefer to bridge external push sources (, , , sockets) into a connection-driven atom, instead of an that wires the observer and writes a sibling result atom.
- Use for lazy external subscriptions/polling that do not depend on the hook target; verify cleanup and abort behavior.
- Do not use to synchronize atoms with other atoms; prefer or .
-
Routing:
- Prefer loaders for route data; loaders are async computeds with .
- 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.
- Flag components that manually check and return ; prefer the route option and layouts/outlets.
- Validate URL params/search with schemas when types matter, and transform string params instead of assuming numbers.
- Confirm route navigation uses and links use where SPA interception is expected.
- Route paths have no leading ; takes params, not a path string.
- Loader takes one merged params/search object. is wrong.
- is a route option; after construction is a computed output, not an assignable callback.
- Callbacks created inside and passed to UI still need .
- Auth, redirect, and feature gates belong in / parent guards, not nullable loader payloads.
- Redirects in guards or URL hooks must be idempotent and prove URL ownership before .
- For index routes under layouts, prefer for active state; stays true for descendants.
-
Abort, sampling, and concurrency:
- and return promises; inside async actions/effects they should be .
- expects controlled promises from , not plain promises.
- 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 already have abort support.
- Check factory dependencies: every read can recreate the inner model. Split volatile inputs, move derivations out, or use / when only some inputs should rebuild the scoped graph.
- Prefer plus for debounce-like behavior.
- Flag component/effect timer bookkeeping (, , local timer handles, manual , unmount ). Prefer abortable
effect(async () => { await wrap(sleep(ms)); ... })
for state-driven timers and action(...).extend(withAbort())
for debounce/throttle commands.
- Do not treat abort rejections as business errors unless the flow explicitly needs that.
-
Forms:
- Prefer , , and for forms.
- Async validation should use , and dependent validation may read other fields reactively.
- Submit handlers should throw errors for and keep payload types explicit.
- Route-bound forms should usually be created in route/scoped factories, not as shared module-level singletons.
- Prefer / for user-facing field values. is the underlying state.
- Put submit mutations in and call ; separate raw submit actions can bypass validation.
-
Persistence and URL sync:
- Prefer Reatom helpers such as , , , or storage-specific persistence extensions over ad hoc effects.
- 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.
- For cached queries, check options against intent: semantics, /length limits, and defaults (true only for empty params). Flag cache on non-idempotent actions.
-
Migration correctness:
- Flag v3-era stale APIs: , , , , , , , , , and .
- Prefer current equivalents: , direct atom reads, ,
action(...).extend(withAsync())
, computed(...).extend(withAsyncData())
, , , , , and .
- 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 ; results are plain values, not callable getters.
- Handwritten UI callbacks that read/write atoms or call actions need , including third-party control callbacks.
- Do not call directly in JSX of a plain function component; create wrapped callbacks inside a Reatom frame or pass them down.
- 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: , , or . Flag bare from a or callback.
- 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.
- Tie /, , (and worker pools), /, and listeners to Reatom lifecycle (async-computed abort, / cleanup, , or ).
- Flag hand-rolled / methods that callers must remember to invoke when / / async-computed abort already express ownership.
- 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 used inside an action/effect only to break a circular import. Dynamic is for code-splitting, not cycle breaking; it also turns a sync flow async and hides the dependency from tracing.
- 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.
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
computed(...).extend(withAsyncData())
.
- Claim says "mutation/command", but code uses async for non-idempotent writes instead of
action(...).extend(withAsync())
.
- Claim says "abort-aware" or "race-safe", but code lacks , , route loader behavior, or around awaited work.
- Claim says sync Reatom writes are wrapped, but code uses
wrap(() => atom.set(...))
without calling/passing the returned function.
- Claim says code preserves context with , but it uses
wrap(() => atom.set(...))()
inside an action/effect/computed where a direct already runs in frame.
- Claim says "abortable fetch", but code does not pass
signal: abortVar.require().signal
to .
- Claim uses , but the action/computed was not extended with .
- Claim says "route loader", but data fetching is placed in a rendered component or guarded with .
- Claim shows route loader params, but the code uses instead of one merged object.
- Claim says route redirect/auth gate, but the code returns nullable loader data instead of blocking in / parent guards.
- Claim says "current Reatom", but snippet uses legacy , , , , or .
- Claim describes a model with relative state/actions, but snippets export separate sibling atoms/actions instead of grouping them through .
- Claim shows an atom/action factory, but the function is named / instead of .
- Claim shows a pure mapper/formatter/normalizer wrapped in , but the function has no IO and does not write state.
- Claim shows derived state as
computed(() => resolveX(model))
with a plain helper that only reads atom getters — split indirection with no non-Reatom reuse.
- Claim says form submit validation, but the code bypasses with a separate raw submit action.
- Heading/prose says one atom/action name while the snippet uses another.
- Snippet omits essential imports such as , , , , , or .
- 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.
- 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.
- Claim says modules are decoupled, but cycles are hidden behind / inside actions.
Typical Mismatches And Fixes
Query Implemented As Imperative 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
.
Chained Incorrectly
Problem:
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.
Used As A Statement (Function Not Called)
Problem:
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.
Pointless IIFE
Problem:
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.
Missing After Async Boundary
Problem:
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.
Callback Calls Reatom Without Context
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.
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.
Abortable 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.
Status 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.
Identity 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.
Thin Computed Wrapper Over Plain Helper
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.
Atom Factory Named Like A 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.
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.
Parallel UI State Instead Of Atomization
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.
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.
Component-Scoped Effect Lifted To Module Scope (Eager Forever)
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 Inside withConnectHook On Its Own Dependency (Infinite Loop)
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.
Object URL Leaked As A Plain String
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.
Compact Gotcha Fixes
- Module-level -> eager at import; start it from route loader, explicit init action, or component mount — not as a forever-connected singleton.
- + that reads the hook target -> infinite connect loop; use a different scope anchor, for gates, or route/init/component mount instead.
- / to break a cycle -> restructure modules or use an orchestration action; dynamic import is for code-splitting.
- Hand-rolled for object URLs/bitmaps/observers/workers -> tie cleanup to / or async-computed abort.
- from a DOM /observer callback -> pass
wrap(() => atom.set(...))
to the callback API, or context.start(() => atom.set(...))
.
wrap(() => atom.set(...))
as a standalone statement -> dead code; call directly or pass externally.
wrap(() => atom.set(...))()
inside action/effect/computed -> pointless; call directly.
computed(() => resolveX(model))
with plain reading atoms -> inline in computed or attach via on the parent.
- to write -> ; positional-call writes on reactive atoms throw.
- without ->
withAsync({ cacheParams: true })
, or retry the computed instead.
.extend(withCache(), withAsync())
-> reorder: / first, after.
- Reading later as data -> action call lists are cleared next tick; store payloads in atoms.
- / -> use the primitive's actions; in-place mutation skips invalidation.
- Synchronous assertion after expecting a subscriber to have fired -> await a microtask; notifications are queued.
- Shared state between tests -> in or scope with .
- -> ; status flags are properties.
- -> ; errors stay on the async target atom.
async loader(params, search)
-> async loader({ q, userId })
; loaders receive one merged params/search object.
- Nullable auth loader ->
params() { return allowed ? {} : null }
; guards should block route ownership before loaders run.
- -> ; route paths are declared without leading .
- Module-level route form singleton -> route/scoped factory or loader-created form when lifetime follows the route.
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.
Action Call List Treated As Durable State
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.
Stale 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.
Finding 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.
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?
- Did you flag bare statements and pointless IIFEs inside Reatom frames?
- Did you challenge imperative fetching, manual routing, and parallel mutable state?
- Did you flag used for pure mappers with no IO or state writes?
- Did you flag thin
computed(() => helper(model))
wrappers where the helper only reads atom getters?
- Did you flag atom/action factories named / instead of ?
- Did you check effect lifetime: module-level eagerness, feature init (route loader / init action / component mount), connect-hook dependency traps, and redundant component subscriptions?
- Did you check that object URLs, bitmaps, workers, and observers are owned by Reatom lifecycle rather than manual ?
- Did you check ordering (async before cache), / option requirements, and key collisions in extensions?
- Did you check write syntax ( vs positional call), in-place mutation of collection primitives, and reliance on ephemeral action call lists?
- Did you check timing assumptions: queued notifications, unsubscribed computed revalidation, and test context isolation via ?
- 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?