原始内容
Overview
atomirx is a reactive state management library that combines the simplicity of atoms with powerful async handling. Built with TypeScript-first design, it provides a complete solution for managing application state with first-class support for async operations, computed values, and React integration.
Key Features
- Atoms - Simple, reactive state containers
- Derived Atoms - Computed values with automatic dependency tracking
- Effects - Side effects that react to state changes
- Pools - Parameterized atom families with automatic garbage collection
- Async-First - Built-in Suspense support for async operations
- React Integration - Hooks and utilities for seamless React usage
- TypeScript - Full type safety with excellent inference
- Tiny - Zero dependencies (except lodash for deep equality)
Installation
npm install atomirx
# or
pnpm add atomirx
# or
yarn add atomirx
Quick Start
import { atom, derived, effect } from 'atomirx';
import { useSelector, rx } from 'atomirx/react';
// Create a mutable atom
const count$ = atom(0);
// Create a derived atom (computed value)
const doubled$ = derived(({ read }) => read(count$) * 2);
// Create a side effect
effect(({ read }) => {
console.log('Count changed:', read(count$));
});
// Use in React
function Counter() {
const count = useSelector(count$);
const doubled = useSelector(doubled$);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count$.set(c => c + 1)}>Increment</button>
</div>
);
}
Table of Contents
Core Concepts
Atoms
Atoms are the fundamental building blocks - simple reactive containers that hold a single value.
import { atom } from 'atomirx';
// Create an atom with initial value
const count$ = atom(0);
// Read the current value
count$.get(); // 0
// Update the value
count$.set(5);
count$.set(prev => prev + 1); // Reducer pattern
// Reset to initial value
count$.reset();
// Subscribe to changes
const unsubscribe = count$.on(() => {
console.log('Count changed:', count$.get());
});
Lazy Initialization
// Value computed at creation time
const timestamp$ = atom(() => Date.now());
// Reset re-runs the initializer
timestamp$.reset(); // Gets new timestamp
Storing Functions
// To store a function as a value, wrap it
const callback$ = atom(() => () => console.log('hello'));
Atom with Context
const data$ = atom((context) => {
// Access AbortSignal for cancellation
const signal = context.signal;
// Register cleanup function
context.onCleanup(() => {
console.log('Cleanup on value change');
});
return fetchData({ signal });
});
Derived Atoms
Derived atoms compute values from other atoms with automatic dependency tracking.
import { derived } from 'atomirx';
const firstName$ = atom('John');
const lastName$ = atom('Doe');
// Simple derived value
const fullName$ = derived(({ read }) =>
`${read(firstName$)} ${read(lastName$)}`
);
// Access the computed value (always returns Promise)
await fullName$.get(); // "John Doe"
// Access stale/cached value synchronously
fullName$.staleValue; // "John Doe" or undefined if not computed yet
// Check current state
fullName$.state(); // { status: "ready", value: "John Doe" }
// Force recomputation
fullName$.refresh();
Derived with Fallback
const posts$ = atom(fetchPosts()); // Async atom
// Without fallback - staleValue can be undefined
const postCount$ = derived(({ read }) => read(posts$).length);
postCount$.staleValue; // number | undefined
// With fallback - staleValue is guaranteed
const postCountSafe$ = derived(
({ read }) => read(posts$).length,
{ fallback: 0 }
);
postCountSafe$.staleValue; // number (0 during loading)
Effects
Effects run side effects in response to atom changes.
import { effect } from 'atomirx';
// Basic effect
const dispose = effect(({ read }) => {
const count = read(count$);
localStorage.setItem('count', String(count));
});
// Stop the effect
dispose();
Effect with Cleanup
effect(({ read, onCleanup }) => {
const interval = read(interval$);
const id = setInterval(() => console.log('tick'), interval);
// Cleanup runs before next execution or on dispose
onCleanup(() => clearInterval(id));
});
Effect with AbortSignal
Effects provide a signal for cancelling async operations:
effect(({ read, signal }) => {
const userId = read(userId$);
// Signal is automatically aborted when effect re-runs or disposes
fetch(`/api/users/${userId}`, { signal })
.then(r => r.json())
.then(user => user$.set(user))
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
});
Pools
Pools are parameterized collections of atoms with automatic garbage collection.
import { pool } from 'atomirx';
// Create a pool with GC after 60 seconds of inactivity
const userPool = pool(
(id: string) => fetchUser(id),
{ gcTime: 60_000 }
);
// Get/set values directly
const user = userPool.get('user-1');
userPool.set('user-1', { name: 'John', email: 'john@example.com' });
// Check existence
userPool.has('user-1'); // true
// Remove entry
userPool.remove('user-1');
// Clear all entries
userPool.clear();
// Subscribe to changes
userPool.onChange((id, value) => console.log('Changed:', id, value));
userPool.onRemove((id, value) => console.log('Removed:', id, value));
Pool in Reactive Context
// Use from() in derived/effect/useSelector
const userPosts$ = derived(({ read, from }) => {
const userId = read(currentUserId$);
const user = read(from(userPool, userId)); // Creates VirtualAtom
return user.posts;
});
Batching
Batch multiple updates to prevent intermediate renders.
import { batch } from 'atomirx';
const firstName$ = atom('');
const lastName$ = atom('');
// Without batch: 2 notifications
firstName$.set('John');
lastName$.set('Doe');
// With batch: 1 notification after both updates
batch(() => {
firstName$.set('John');
lastName$.set('Doe');
});
Modules (define)
Create swappable lazy singleton stores for better organization and testing.
import { define, atom, readonly } from 'atomirx';
const counterModule = define(() => {
const count$ = atom(0);
return {
// Expose as read-only to consumers
count$: readonly(count$),
// Actions
increment: () => count$.set(c => c + 1),
decrement: () => count$.set(c => c - 1),
reset: () => count$.reset(),
};
});
// Usage
const { count$, increment } = counterModule();
increment();
Override for Testing
// In tests
beforeEach(() => {
counterModule.override(() => ({
count$: atom(999),
increment: vi.fn(),
decrement: vi.fn(),
reset: vi.fn(),
}));
});
afterEach(() => {
counterModule.reset();
});
React Integration
useSelector
React hook for selecting values from atoms with automatic subscriptions.
import { useSelector } from 'atomirx/react';
function Counter() {
// Single atom
const count = useSelector(count$);
// Derived value with selector
const doubled = useSelector(({ read }) => read(count$) * 2);
// Multiple atoms
const fullName = useSelector(({ read }) =>
`${read(firstName$)} ${read(lastName$)}`
);
return <div>{count} - {doubled} - {fullName}</div>;
}
rx
Inline reactive component for fine-grained updates.
import { rx } from 'atomirx/react';
function Counter() {
// Only the rx component re-renders when count changes
return (
<div>
Count: {rx(count$)}
Doubled: {rx(({ read }) => read(count$) * 2)}
<button onClick={() => count$.set(c => c + 1)}>+</button>
</div>
);
}
rx with Loading/Error Handling
{rx(
({ read }) => read(asyncAtom$),
{
loading: () => <Spinner />,
error: ({ error }) => <Error message={error.message} />,
}
)}
useAction
Hook for managing async operations with state tracking.
import { useAction } from 'atomirx/react';
function SaveButton() {
const [save, state] = useAction(async (data: FormData) => {
const result = await api.save(data);
return result;
});
return (
<button
onClick={() => save(formData)}
disabled={state.status === 'loading'}
>
{state.status === 'loading' ? 'Saving...' : 'Save'}
</button>
);
}
useStable
Hook for creating stable references to callbacks and objects. Replaces useCallback and useMemo.
import { useStable } from 'atomirx/react';
function Component({ onSave, data }) {
const stable = useStable({
// Stable callback references
handleSave() {
onSave(data);
},
handleCancel() {
console.log('cancelled');
},
// Stable object reference
config: { timeout: 5000 },
// Stable array reference
columns: [{ key: 'name', label: 'Name' }],
});
return <Child onSave={stable.handleSave} config={stable.config} />;
}
Async Operations
Suspense Support
atomirx uses a Suspense-style API for async atoms.
const user$ = atom(fetchUser());
function UserProfile() {
// Throws Promise when loading (suspends)
// Throws error when rejected
// Returns value when resolved
const user = useSelector(user$);
return <div>{user.name}</div>;
}
// Must wrap with Suspense and ErrorBoundary
function App() {
return (
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}
Async Utilities
The select context provides utilities for handling multiple async atoms.
// Wait for all atoms (like Promise.all)
const dashboard$ = derived(({ all }) => {
const [user, posts] = all(user$, posts$);
return { user, posts };
});
// First settled value (like Promise.race)
const fastest$ = derived(({ race }) => race(source1$, source2$));
// First success, ignore errors unless all fail (like Promise.any)
const anySuccess$ = derived(({ any }) => any(primary$, fallback$));
// All settled with status (like Promise.allSettled)
const results$ = derived(({ settled }) => {
const results = settled(atom1$, atom2$);
return results.map(r =>
r.status === 'ready' ? r.value : 'failed'
);
});
Promise State Tracking
import { isPending, isFulfilled, isRejected, trackPromise } from 'atomirx';
const promise = fetchData();
const state = trackPromise(promise);
if (isPending(promise)) {
console.log('Loading...');
}
if (isFulfilled(promise)) {
console.log('Value:', state.value);
}
if (isRejected(promise)) {
console.log('Error:', state.error);
}
Advanced Topics
Select Context
All reactive contexts (derived, effect, useSelector, rx) provide a unified select context.
interface SelectContext {
// Read a single atom's value
read<T>(atom: Atom<T>): Awaited<T>;
// Get atom from pool
from<P, T>(pool: Pool<P, T>, params: P): VirtualAtom<T>;
// Wait for all atoms
all<A extends Atom[]>(...atoms: A): Values<A>;
// First settled (like Promise.race)
race<A extends Atom[]>(...atoms: A): Value<A[number]>;
// First success (like Promise.any)
any<A extends Atom[]>(...atoms: A): Value<A[number]>;
// All settled with status
settled<A extends Atom[]>(...atoms: A): SettledResult<A>[];
// Safe error handling (preserves Suspense)
safe<T>(fn: () => T): [Error, undefined] | [undefined, T];
// Check if atom is ready (non-loading)
ready<T>(atom: Atom<T>): boolean;
}
Equality Functions
Control when atoms notify subscribers.
// Built-in options
atom(value, { equals: 'strict' }); // Object.is (default)
atom(value, { equals: 'shallow' }); // Shallow comparison
atom(value, { equals: 'deep' }); // Deep comparison (lodash)
// Custom equality function
atom(value, {
equals: (a, b) => a.id === b.id
});
Error Handling
Use safe() instead of try/catch in reactive contexts.
// ❌ WRONG - Catches Suspense Promise
derived(({ read }) => {
try {
return read(asyncAtom$);
} catch (e) {
return 'fallback'; // Catches loading promises too!
}
});
// ✅ CORRECT - Use safe()
derived(({ read, safe }) => {
const [err, data] = safe(() => read(asyncAtom$));
if (err) return { error: err.message };
return { data };
});
DevTools & Hooks
Register hooks for debugging and monitoring.
import { onCreateHook, onErrorHook } from 'atomirx';
// Monitor atom creation
const unsubscribe = onCreateHook.add((info) => {
console.log('Created:', info.type, info.key);
});
// Monitor errors
onErrorHook.add(({ source, error }) => {
console.error('Error in', source.type, source.key, error);
});
API Reference
Core
| Export | Description |
|---|---|
atom(value, options?) |
Create a mutable atom |
derived(selector, options?) |
Create a computed atom |
effect(fn, options?) |
Create a side effect |
pool(init, options) |
Create a parameterized atom family |
batch(fn) |
Batch multiple updates |
define(factory, options?) |
Create a lazy singleton module |
readonly(atom) |
Type utility for read-only exposure |
select(selector) |
Run a selector outside React |
emitter() |
Create an event emitter |
React
| Export | Description |
|---|---|
useSelector(selector, equals?) |
Subscribe to atom(s) |
rx(selector, options?) |
Inline reactive component |
useAction(fn, options?) |
Async action with state |
useStable |
Stable reference utilities |
Promise Utilities
| Export | Description |
|---|---|
trackPromise(promise) |
Get/track promise state |
isPending(value) |
Check if promise is loading |
isFulfilled(value) |
Check if promise resolved |
isRejected(value) |
Check if promise rejected |
unwrap(value) |
Get value or throw error |
Type Guards
| Export | Description |
|---|---|
isAtom(value) |
Check if value is an atom |
isDerived(value) |
Check if value is a derived atom |
isPool(value) |
Check if value is a pool |
isVirtualAtom(value) |
Check if value is a virtual atom |
Documentation
For more detailed documentation, see:
- Core Concepts - Deep dive into atoms, derived, effects
- React Guide - Complete React integration guide
- Async Patterns - Handling async operations
- Testing - Testing strategies and mocking
- Migration Guide - Migrating from other libraries
- API Reference - Complete API documentation
Examples
Check out the example applications:
- Showcase App - Interactive demos of all features
- Todo App - Real-world todo application
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
License
MIT © Gignuyen