linq2js/atomirx
Overview
This skill guides developers on using atomirx, an opinionated reactive state management system for TypeScript and React. It explains core primitives (atom, derived, effect, event, pool), SelectContext utilities (read, ready, race, all, state), React hooks (useSelector, rx, useAction, useStable), and debugging patterns including DevTools bootstrap and meta.key. The guide focuses on practical rules, patterns, and anti-patterns for predictable reactive flows.
How this skill works
atomirx treats async and sync values uniformly inside reactive contexts: you read() synchronously and Suspense handles Promises. The SelectContext provides composable primitives (read, ready, all, race, safe, state) usable inside derived(), effect(), useSelector(), and rx(). Events block until fired and pools provide parameterized atoms with GC. DevTools must initialize before atoms to track creation.
When to use it
- Use atomirx when you need a Suspense-friendly reactive store that unifies async/sync reads.
- Use event() for user-driven signals that should block until fired (forms, confirmations).
- Use pool() for parameterized data (per-id caches) instead of manual Maps.
- Use derived() for computed values and effect() for eager side effects tied to reactive dependencies.
- Use useSelector to read multiple atoms in one selector to avoid extra subscriptions.
Best practices
- Always wrap global state and factories in define() to enable lazy singletons and proper scoping.
- Provide meta.key for every atom/derived/effect/pool to ensure reliable DevTools and debugging.
- Group related reads into a single useSelector call; avoid multiple useSelector calls in the same component.
- Use batch() when performing multiple atom.set() calls to emit a single update.
- Never try/catch around read(); use safe() to preserve Suspense behavior and avoid swallowing async control flow.
- Always use useStable() for stable callbacks/configs instead of React useCallback/useMemo.
Example use cases
- Form workflow: use event() to suspend derived/effect until submitEvent.fire(data).
- User cache: create a pool(fetchUser) with gcTime and use from(pool, id) inside derived selectors.
- Combined loading: use all() or race() in a derived to coordinate API and cache results with ready() to wait for non-null values.
- Batch updates: update user, settings, and lastUpdated inside batch() to avoid multiple renders.
- Service actions: implement useAction with atom deps and call .get() inside the action for up-to-date values.
FAQ
read() returns values and throws a Promise if loading; ready() suspends until a non-null result and preserves narrowing for discriminated unions.
When should I use event() vs atom<Promise<T>>?
Use event() for user-driven signals and one-time or repeated meaningful fires; use atom<Promise<T>> for representing ongoing async state like fetches tied to an atom.