This doc follows the source-first approach in docs/README.md.
This guide covers extension points in general, not only withConnectHook.
Use extensions to add behavior at the atom/action boundary: validation, persistence, async lifecycle, side effects, URL sync, and feature-specific APIs.
| File | Why read it |
|---|---|
src/shared/model/locale.ts |
withParams + withLocalStorage + withChangeHook composition |
src/shared/model/theme.ts |
enum coercion and persisted preferences |
src/pages/items/model/filters.ts |
URL-bound filters via withSearchParams |
src/entities/conversation/model/unreadCount.ts |
async read model with withAsyncData |
src/shared/model/documentTitle.ts |
connect-scoped side effects with withConnectHook + addChangeHook |
src/pages/timer/model/model.ts |
action lifecycle with withAbort and change hooks |
src/shared/model/headerTrail.ts |
custom domain extension (withMatch*) with module-level registration |
https://github.com/reatom/reatom/blob/v1001/packages/core/src/extensions/withConnectHook.ts |
connect/disconnect runtime behavior |
https://github.com/reatom/reatom/blob/v1001/packages/core/src/extensions/withChangeHook.ts |
state-change hook scheduling semantics |
The
@reatom/corepackage ships only compileddist/output, so thewithConnectHook/withChangeHooksources above point at the upstreamv1001branch.
- Name every extension-created primitive (
effect,computed,action). - Compose small extensions in a deterministic order (parse/coerce before persistence, persistence before side effects).
- Keep extension public API narrow; expose domain helpers, keep low-level internals private.
- Put mutable runtime state in per-connection/per-run scope, not module-level extension closures.
- Make cleanup idempotent and identity-safe when touching shared/global atoms.
- Do not create reactive subscription to
targetinsidewithConnectHook(avoideffect(() => target())there). - For connect-scoped tracking of target changes, use
addChangeHook(target, cb)and always call returnedunhookin cleanup. - Keep loaders for data fetching; route UI side effects should live in route extension points (for example
route.match.extend(...)).
| Need | Primary extension(s) | Example |
|---|---|---|
| Normalize input before write | withParams, custom coercion |
src/shared/model/locale.ts |
| Persist state | withLocalStorage/withSessionStorage |
src/shared/model/topBar.ts, src/shared/model/theme.ts |
| React to value changes | withChangeHook |
src/shared/model/locale.ts, src/pages/timer/model/model.ts |
| React to action calls | withCallHook |
Use when behavior depends on action invocations, not atom value |
| Resource lifecycle on first/last subscriber | withConnectHook/withDisconnectHook |
src/shared/model/documentTitle.ts |
Async state (pending/data/error/retry) |
withAsync, withAsyncData |
src/entities/conversation/model/unreadCount.ts |
| Concurrency/abort strategy | withAbort |
src/pages/timer/model/model.ts |
| URL query synchronization | withSearchParams |
src/pages/items/model/filters.ts |
| Custom cross-cutting behavior | custom Ext<T> + helpers |
src/shared/model/headerTrail.ts |
- If you need state normalization, start with
withParamsor coercion helper. - If you need persistence, add
withLocalStorage/withSessionStorage. - If you need reactions to value changes, use
withChangeHook. - If you need reactions to calls/events, use
withCallHook. - If you need subscriber lifecycle resources, use
withConnectHook. - If you need pending/error/data, use
withAsync*. - If you need cancellation policy, add
withAbort.
- Type the target explicitly (
Ext<...>). - Keep helper internals private; export only domain entry points.
- Allocate mutable runtime state inside lifecycle callback scope.
- Start local reactive sync (
effect) only when needed. - Return cleanup that unsubscribes and reverts side effects.
- Guard shared state cleanup by identity if parallel flows can overlap.
- In
withConnectHook, create localdisposestate for this connection. - Do one initial sync from current value (
sync(target())), without creating reactive subscription. - Add dynamic change hook with
addChangeHook(target, sync). - In cleanup, call
unhook()and then currentdispose. - Do not keep cross-context mutable
disposein outer extension closure.
- Verify extension order effects (especially parse/persist/side-effect).
- Verify rapid state transitions and cleanup ordering.
- Verify behavior under multiple contexts if your project uses
clearStack/custom contexts. - Run
hk checkfor validation, orhk fixfirst if formatting/lint fixes are expected.
clearStackin tests can hide production-time context overlap bugs.- Shared mutable closure state in extension factories can collide across contexts.
withChangeHookalone is not a connect/disconnect lifecycle primitive.- Forgetting
unhook()fromaddChangeHookwill leak middleware permanently. - URL/persistence extensions can create cross-tab or history side effects; use intentionally.
- Pure API helpers are not extension points; keep Reatom imports at model, route, and UI binding boundaries, except the API transport may read
abortVar.get()for request cancellation so fetches are abortable by the frame that issues them; explicit signals override the frame signal. - For long-lived async resources, isolate scope with
withConnectHookcleanup and abort-aware flows.