atomirx

atomirx State Management

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "atomirx" with this command: npx skills add linq2js/atomirx/linq2js-atomirx-atomirx

atomirx State Management

Philosophy: You Don't Care About Async/Sync

Atomirx abstracts away the async/sync distinction. In reactive contexts, you write sync code regardless of whether atoms contain sync values or Promises.

Context Your Code Async Handling

useSelector , derived , effect

Sync — just read()

Suspense handles it

Services Receive values as parameters Don't read atoms

Outside reactive context await .get()

Explicit when needed

// You don't care if user$ contains sync value or Promise const userName$ = derived(({ read }) => read(user$).name);

// Same pattern works for any atom function MyComponent() { const name = useSelector(userName$); // Sync code, Suspense handles async return <div>{name}</div>; }

This is why naming doesn't use Async$ suffix — the abstraction makes it irrelevant.

Bootstrap Pattern (DevTools)

DevTools must initialize BEFORE any atoms are created to properly track all reactive primitives.

// main.tsx async function main() { // 1. Render devtools FIRST (before any atom imports) if (process.env.NODE_ENV !== 'production') { const { renderDevtools } = await import("atomirx/react-devtools"); await renderDevtools(); }

// 2. THEN import React and App (which contains atoms) const React = await import("react"); const ReactDOM = await import("react-dom/client"); const { default: App } = await import("./App");

// 3. Render app ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <App /> </React.StrictMode> ); }

main();

Why this order matters:

  • onCreateHook must be set up before atoms are created

  • DevTools uses onCreateHook to track atom/derived/effect creation

  • If atoms are imported before devtools, they won't appear in the panel

For apps with bootstrap logic:

// bootstrap.ts export async function bootstrap() { // Initialize services, fetch config, etc. await initializeServices(); }

// main.tsx async function main() { // 1. DevTools first if (process.env.NODE_ENV !== 'production') { const { renderDevtools } = await import("atomirx/react-devtools"); await renderDevtools(); }

// 2. Bootstrap (may create atoms) const { bootstrap } = await import("./bootstrap"); await bootstrap();

// 3. Import and render app const React = await import("react"); const ReactDOM = await import("react-dom/client"); const { default: App } = await import("./App");

ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <App /> </React.StrictMode> ); }

main();

Core Primitives

Primitive Purpose Subscription

atom<T>(initial)

Mutable state No

derived(fn)

Computed value Yes (lazy)

effect(fn)

Side effects on changes Yes (eager)

event<T>(opts)

Block until fired Yes

select(fn)

One-time read, no subscription No

pool(fn, opts)

Parameterized atoms with GC Per-entry

batch(fn)

Group updates, single notify No

define(fn)

Lazy singleton factory No

onCreateHook

Track atom/effect creation No

onErrorHook

Global error handling No

Events (Block Until Fired)

Events block computation until fire() is called. Useful for user-driven workflows.

event vs atom<Promise<T>>

Aspect atom<Promise<T>>

event<T>

Initial state Promise you provide Pending (no data)

Set value .set(promise)

.fire(data)

First update Replaces promise Resolves pending

Get last value Track yourself .last() built-in

Use case Async data fetching User signals

Event API

Method Description

fire(payload)

Fire event with payload

get()

Current promise (may be resolved)

next()

Pending promise for next meaningful fire

on(listener)

Subscribe to changes

last()

Last fired payload

fireCount

Count of meaningful fires

sealed()

True if once and already fired

get() vs next()

get()

next()

Returns Current promise Pending promise

After fire Same resolved New pending

Use case Reactive Imperative

next() respects equals

  • duplicate fires won't resolve.

import { event, derived, effect } from "atomirx";

// Create event const submitEvent = event<FormData>({ meta: { key: "form.submit" } }); const cancelEvent = event({ meta: { key: "form.cancel" } }); // void payload

// In derived - suspends until fire() const result$ = derived(({ read }) => { const data = read(submitEvent); // Blocks until submitEvent.fire(data) return processForm(data); });

// In effect - suspends until fire() effect(({ read }) => { const data = read(submitEvent); console.log("Form submitted:", data); });

// Race multiple events const outcome$ = derived(({ race }) => { const { key, value } = race({ submit: submitEvent, cancel: cancelEvent, }); return key === "cancel" ? null : processForm(value); });

// Fire the event (from user action) function handleSubmit(data: FormData) { submitEvent.fire(data); }

// Imperative awaiting with next() async function processClicks() { while (true) { const data = await clickEvent.next(); handleClick(data); } }

once Option (One-Time Events)

const initEvent = event<Config>({ once: true });

initEvent.fire(config); // Promise resolved initEvent.fire(config2); // No-op (already fired) initEvent.get(); // Same resolved promise initEvent.next(); // Same resolved promise

// Late subscriber - triggers immediately initEvent.on(() => console.log("init done")); // Logs immediately

Behavior once: false

once: true

Multiple fire()

New promises First only

next() after fire New pending Same resolved

on() after fire Normal Triggers immediately

Key Points:

  • read(event) suspends until fire() is called (like waiting for staleValue)

  • Works with race() , all() for waiting on multiple user actions

  • Each fire() after the first creates a new promise → triggers reactive updates

  • Use equals option to dedupe identical payloads

  • Use once: true for one-time events (init, login)

  • NOT for promise flow control (timeouts, retries) — use abortable() for those

SelectContext Methods

Works identically in derived() , effect() , useSelector() , rx() . Learn once, use everywhere.

Method Signature Behavior

read()

read(atom$)

Read + track dependency

ready()

ready(atom$) or ready(array)

Wait for non-null (suspends)

from()

from(pool, params)

Get ScopedAtom from pool

track()

track(atom$)

Track without reading

untrack()

untrack(atom$) or fn Read/exec without tracking

safe()

safe(() => expr)

Catch errors, preserve Suspense

all()

all([a$, b$])

Wait for all (Promise.all)

any()

any({ a: a$, b: b$ })

First ready (discriminated union)

race()

race({ a: a$, b: b$ })

First settled (discriminated union)

settled()

settled([a$, b$])

All results (Promise.allSettled)

state()

state(atom$)

Get state without throwing

and()

and([cond1, cond2])

Logical AND, short-circuit

or()

or([cond1, cond2])

Logical OR, short-circuit

// Same pattern works everywhere const pattern = ({ read, all, safe }) => { const [user, posts] = all([user$, posts$]); const [err, parsed] = safe(() => JSON.parse(read(config$))); return { user, posts, config: err ? null : parsed }; };

const combined$ = derived(pattern); const data = useSelector(pattern); effect(pattern); { rx(pattern); }

race()/any() Discriminated Union

race() and any() return discriminated unions — checking key narrows value type:

const result = race({ num: numAtom$, str: strAtom$ });

if (result.key === "num") { result.value; // narrowed to number } else { result.value; // narrowed to string }

// Tuple destructuring also works const [winner, value] = result;

ready() with Async Utilities

Use ready() to ensure values from all() , race() , or any() are non-null:

// ready() + all() — suspend if any value is null/undefined const [user, posts] = ready(all([user$, posts$]));

// ready() + race() — suspend if winning value is null, preserves narrowing const result = ready(race({ cache: cache$, api: api$ })); if (result.key === "cache") { result.value; // narrowed to cache type (non-null) }

read() vs ready() vs state()

Method On null/undefined On loading Use Case

read()

Returns null Throws Promise Always need value

ready()

Suspends Throws Promise Wait for data

state()

Returns state obj Returns state Manual loading/error

Key Rules

  • MUST use define() for all state/logic. Global classes OK, variables MUST be in define() .

  • MUST use batch() for multiple atom updates.

  • MUST group useSelector calls into single selector.

  • useAction deps: pass atoms, use .get() inside for auto re-dispatch.

  • NEVER try/catch with read() — breaks Suspense. Use safe() .

  • MUST co-locate mutations in store that owns the atom.

  • MUST export readonly atoms via readonly({ atom$ }) .

  • SelectContext is sync only — NEVER use in setTimeout/Promise.then.

  • Services vs Stores — Services are stateless, Stores have atoms.

  • NEVER import service factories — use define() , invoke with () .

  • Single effect, single workflow — split multiple workflows.

  • MUST define meta.key for debugging: { meta: { key: "store.name" } } .

  • MUST use .override() for hooks, never assign .current directly.

  • MUST use useStable() — NEVER use React's useCallback.

  • Use pool for parameterized state instead of manual Maps.

meta.key (REQUIRED)

// ✅ DO: Define meta.key const user$ = atom<User | null>(null, { meta: { key: "auth.user" } }); const isAuth$ = derived(({ read }) => !!read(user$), { meta: { key: "auth.isAuthenticated" } }); effect(({ read }) => { ... }, { meta: { key: "auth.persistSession" } }); const userPool = pool((id: string) => fetchUser(id), { gcTime: 60_000, meta: { key: "users" } });

// ❌ DON'T: Skip meta.key const user$ = atom<User | null>(null);

useSelector Grouping

// ✅ DO: Single useSelector const { user, posts, settings } = useSelector(({ read }) => ({ user: read(user$), posts: read(posts$), settings: read(settings$), }));

// ❌ DON'T: Multiple calls const user = useSelector(user$); const posts = useSelector(posts$);

useAction with Atoms

// ✅ DO: Pass atoms to deps, use .get() inside const load = useAction(async () => atom1$.get() + (await atom2$.get()), { deps: [atom1$, atom2$], lazy: false, });

// ❌ DON'T: useSelector values in deps const { v1, v2 } = useSelector(({ read }) => ({ v1: read(atom1$), v2: read(atom2$), })); const load = useAction(async () => v1 + v2, { deps: [v1, v2], lazy: false });

batch() for Multiple Updates

// ✅ DO: Batch batch(() => { user$.set(newUser); settings$.set(newSettings); lastUpdated$.set(Date.now()); });

// ❌ DON'T: Separate updates user$.set(newUser); settings$.set(newSettings);

useStable() (REQUIRED)

MUST use useStable() instead of React's useCallback /useMemo .

// ❌ FORBIDDEN const handleSubmit = useCallback( () => auth.register(username), [auth, username] );

// ✅ REQUIRED const stable = useStable({ onSubmit: () => auth.register(username), onLogin: () => auth.login(), config: { timeout: 5000, retries: 3 }, columns: [{ key: "name", label: "Name" }], });

pool() for Parameterized State

// ✅ DO: Use pool const userPool = pool((id: string) => fetchUser(id), { gcTime: 60_000, meta: { key: "users" }, }); userPool.get("user-1"); userPool.set("user-1", newUser);

// In reactive context use from() const userPosts$ = derived(({ read, from }) => { const user$ = from(userPool, "user-1"); return read(user$).posts; });

// ❌ DON'T: Manual Map const userCache = new Map<string, MutableAtom<User>>();

and()/or() for Boolean Logic

// ✅ DO: Use and()/or() const canEdit$ = derived(({ and }) => and([isLoggedIn$, hasPermission$])); const hasData$ = derived(({ or }) => or([cacheData$, apiData$]));

// Lazy evaluation const canDelete$ = derived(({ and }) => and([ isLoggedIn$, () => hasDeletePermission$, // Only evaluated if logged in ]) );

// ❌ DON'T: Manual logic const canEdit$ = derived( ({ read }) => read(isLoggedIn$) && read(hasPermission$) );

untrack() for Non-Reactive Reads

// ✅ DO: Use untrack() when you need to read without re-computing const combined$ = derived(({ read, untrack }) => { const count = read(count$); // Tracks count$ - re-computes on change const config = untrack(config$); // Does NOT track - no re-compute on change return count * config.multiplier; });

// Also works with functions for multiple reads const snapshot$ = derived(({ read, untrack }) => { const liveData = read(liveData$); // Tracked const snapshot = untrack(() => { // None of these are tracked return { a: read(a$), b: read(b$), c: read(c$) }; }); return { liveData, snapshot }; });

define() for Services and Stores

// ✅ STORE (has atoms) export const counterStore = define(() => { const count$ = atom(0, { meta: { key: "counter.count" } }); return { ...readonly({ count$ }), increment: () => count$.set((x) => x + 1), }; });

// ✅ SERVICE (stateless) export const storageService = define( (): StorageService => ({ get: (key) => localStorage.getItem(key), set: (key, val) => localStorage.setItem(key, val), }) );

// ❌ FORBIDDEN: Factory pattern import { getAuthService } from "@/services/auth"; const auth = getAuthService(); // WRONG

// ✅ REQUIRED: Module invocation import { authService } from "@/services/auth.service"; const auth = authService(); // Correct

Hooks (.override() REQUIRED)

// ❌ FORBIDDEN: Direct assignment onCreateHook.current = (info) => { ... };

// ✅ REQUIRED: Use .override() onCreateHook.override((prev) => (info) => { prev?.(info); console.log(Created ${info.type}: ${info.key}); });

onErrorHook.override((prev) => (info) => { prev?.(info); Sentry.captureException(info.error); });

Finding Things

To Find Search Pattern

Atom definitions atom< or atom(

Derived atoms derived((

Effects effect((

Pools pool((

Stores define(() => in *.store.ts

Services define(() => in *.service.ts

Atom usages read( , ready( , all([ , any({ , race({ , settled([

Non-reactive reads untrack(

Pool usages from(poolName,

Mutations Find store owner, check return statement

Hook setup onCreateHook.override , onErrorHook.override

Debugging "Why doesn't X update?"

  • Find atom being set → search .set(

  • Find subscribers → search read(atomName$) , ready(atomName$)

  • Check derived is subscribed → used in useSelector ?

  • Check effect cleanup → doesn't prevent re-run?

  • For pools → check if entry was GC'd, verify from() usage

Common Issues

Symptom Likely Cause Fix

Derived never updates No active subscription Use useSelector in component

Effect runs infinitely Setting atom it reads Use select() for non-reactive

ready() never resolves Value never becomes non-null Check data flow

Stale closure Reading atom in callback Use .get() in callbacks

Suspense not working try/catch around read() Use safe() instead

Hook not firing Direct .current assign Use .override() instead

Missing hook calls Hook chain broken Always call prev?.(info)

Pool entry missing GC'd before access Increase gcTime

ScopedAtom error Used outside context Only use from() inside derived

Too many re-computes Tracking unnecessary deps Use untrack() for config

DevTools missing atoms Atoms created before hook Use bootstrap pattern (see above)

Naming Conventions

Type Variable File Contains

Service authService

auth.service.ts

Pure functions only

Store authStore

auth.store.ts

Atoms, derived, effects

Type Suffix Example

Atom (sync or async) $

count$ , user$ , products$

Derived $

doubled$ , userName$

Pool Pool

userPool , productPool

Service Service

authService (NO atoms)

Store Store

authStore (HAS atoms)

Actions verb-led navigateTo , invalidate

Why no Async$ ? Atomirx abstracts async/sync — you don't care in SelectContext (Suspense handles it).

File Structure

src/ ├── services/ # Stateless │ ├── auth/ │ │ └── auth.service.ts │ └── crypto/ │ └── crypto.service.ts └── stores/ # Stateful ├── auth.store.ts ├── todos.store.ts └── sync.store.ts

References

  • Rules & Best Practices

  • Pool Patterns

  • Select Context

  • Deferred Loading

  • React Integration

  • Error Handling

  • Async Patterns

  • Atom Patterns

  • Derived Patterns

  • Effect Patterns

  • Hooks

  • Testing Patterns

  • Store Template

  • Service Template

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

Dingding

钉钉开放平台开发助手,精通机器人、审批流程、日程管理等企业 API

Registry SourceRecently Updated
General

Takeout Coupon 外卖优惠券隐藏券大额券,美团、京东、闪购/饿了么

调用外卖优惠券API获取各平台(美团、淘宝闪购/饿了么、京东)的隐藏外卖券列表及聚合领券页面。返回优惠券代码和领取说明,用户可复制优惠码到对应APP领取。

Registry SourceRecently Updated
General

AI Rankings Leaderboard (AI 排行榜)

Comprehensive AI leaderboard for LLM models and AI applications. Query model rankings, model IDs, and pricing from OpenRouter and Pinchbench. Trigger words i...

Registry SourceRecently Updated