npm package: @react-sky/atom
Description: React state library with atoms as primitive units of state
Latest version: 0.0.1-alpha.1

# @react-sky/atom

React state library with atoms as primitive units of state.

## Introduction

`@react-sky/atom` is a lightweight, atom-based state management library for React. It provides fine-grained reactivity through composable state atoms with full TypeScript support, Suspense integration, and SSR capabilities.

### Key Features

- **Primitive Atoms**: Base state atoms with initial values
- **Derived Atoms**: Computed values from dependencies (read-only and writable)
- **Store Snapshots**: Serializable state snapshots for SSR hydration
- **React Hooks**: First-class integration with React 18+ and Suspense
- **Transactions & Batching**: Atomic updates with rollback support
- **Actions**: Action-based atoms with optimistic updates and error handling
- **Queue Utilities**: FIFO operation queues with concurrency control
- **Computed Helpers**: Advanced derivation utilities with memoization
- **Focus/Lens Utilities**: Nested state manipulation through optics

## Installation

```bash
pnpm add @react-sky/atom
```

Peer dependency:
```bash
pnpm add react@^18
```

## Quick Start

```tsx
import { atom, useAtom } from '@react-sky/atom';

// Create a state atom
const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  
  return (
    <div>
      <button onClick={() => setCount(count - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}
```

## Concepts

### Atoms

Atoms are the primitive units of state in this library. There are three types:

- **Base Atoms**: Atoms with initial values (e.g., `atom(42)`)
- **Derived Atoms**: Read-only computed values from dependencies
- **Writable Derived Atoms**: Computed values that can update their dependencies

### Store

The store manages atom state and subscriptions. The library provides:
- `createStore()` - Create a new store instance
- `defaultStore` - Global default store
- `snapshot()/restore()` - Serializable state for SSR

### Provider

The `Provider` component allows you to wire a custom store through React context. All hooks fall back to the global default store if no Provider is present.

### Suspense vs Non-Suspense

Atoms can throw Promises for async operations. By default, hooks assume Suspense usage and propagate these Promises. You can opt out using `useAtomValue({ suspense: false })` to handle async state manually.

### SSR Hydration

On the server, create a store and snapshot the state:
```tsx
const store = createStore();
// ... populate atoms
const snapshot = store.snapshot();
```

On the client, restore from the snapshot:
```tsx
const store = createStore({ snapshot });
```

## API Reference

### Core

#### `atom<T>(initialValue: T, config?: AtomConfig<T>): BaseWritableAtom<T>`
Creates a base atom with an initial value. Returns a writable atom.

```tsx
const counter = atom(0);
const namedAtom = atom(0, { debugName: 'counter' });
```

#### `atom<T>(read: Read<T>, config?: AtomConfig<T>): BaseAtom<T>`
Creates a read-only derived atom from a read function.

```tsx
const doubled = atom((get) => get(counter) * 2);
```

#### `atom<T, Args>(read: Read<T>, write: Write<Args>, config?: AtomConfig<T>): BaseWritableAtom<T, Args>`
Creates a writable derived atom with both read and write functions.

```tsx
const expensive = atom(
  (get) => get(counter) * 2,
  (getCommit set, value: number) => set(counter, value / 2)
);
```

#### Type Helpers
```tsx
import type { 
  Atom, 
  WritableAtom, 
  AtomConfig, 
  Getter, 
  Setter,
  Read,
  Write 
} from '@react-sky/atom';

// Type guards
isWritableAtom(atom); // atom is WritableAtom
hasInitialValue(atom); // atom has initial value
```

### Store

#### `createStore(options?: { snapshot?: StoreSnapshot }): Store`
Creates a new store instance, optionally restoring from a snapshot.

#### `defaultStore: Store`
Global store instance used by default.

#### `store.snapshot(): StoreSnapshot`
Creates a serializable snapshot of current state for SSR.

#### `store.restore(snapshot: StoreSnapshot): void`
Restores state from a snapshot.

#### SSR Helpers
```tsx
import { createSSRStore, type SSRSnapshot } from '@react-sky/atom';

// Server-side
const store = createStore();
const snapshot: SSRSnapshot = store.snapshot();

// Client-side
const store = createStore({ snapshot });
```

### React Hooks

#### `useAtom<T>(atom: Atom<T>, options?: { suspense: boolean }): [T, never]`
#### `useAtom<T, Args>(atom: WritableAtom<T, Args>, options?: { suspense: boolean }): [T, (...args: Args) => void]`
Primary hook returning atom value and setter (if writable).

```tsx
const [count, setCount] = useAtom(counter);
const [doubled] = useAtom(readOnlyDerived);
```

#### `useAtomValue<T>(atom: Atom<T>, options?: { suspense: boolean }): T | AsyncState<T>`
Hook for read-only access. When `suspense:(false), returns { loading, error, data }` for async atoms.

```tsx
const count = useAtomValue(counter); // T
const asyncResult = useAtomValue(asyncAtom, { suspense: false }); 
// { loading: boolean; error: Error | null; data: T | undefined }
```

#### `useSetAtom<T, Args>(atom: WritableAtom<T, Args>): (...args: Args) => void`
Hook returning只有setter function.

```tsx
const increment = useSetAtom(counter);
```

#### `useAtomCallback<T>(atom: Atom<T>, callback: (value: T, prevValue: T) => void, deps?: React.DependencyList): void`
Subscribe to atom changes without causing re-renders.

```tsx
useAtomCallback(userAtom, (user) => {
  analytics.track('user_updated', user);
}, []);
```

#### `useAtomSnapshot<T>(atoms: T): { [K in keyof T]: T[K] extends Atom<infer U> ? U : never }`
Get values from multiple atoms in a single snapshot.

```tsx
const state = useAtomSnapshot({
  user: userAtom,
  settings: settingsAtom,
  theme: themeAtom
});
```

#### `useResetAtom<T>(atom: WritableAtom<T>): () => void`
Returns a function that resets the atom to its initial value.

```tsx
const resetUser = useResetAtom(userAtom);
// Call without arguments to reset
resetUser();
```

#### `useDerivedAtomValue<T, Deps>(deriveFn: (values: ComputedValues<Deps>) => T, deps: Deps): T` ⚠️ **Deprecated**
Custom hook for derived state based on atom dependencies.

**⚠️ Deprecated:** Use derived atoms with `useAtomValue` instead for better performance and consistency.

```tsx
// ❌ Deprecated approach
const fullName = useDerivedAtomValue(
  ([user]) => `${user.firstName} ${user.lastName}`,
  [userAtom]
);

// ✅ Recommended approach
const fullNameAtom = atom((get) => {
  const user = get(userAtom);
  return `${user.firstName} ${user.lastName}`;
});
const fullName = useAtomValue(fullNameAtom);
```

### Provider

```tsx
import { Provider } from '@react-sky/atom';

function App() {
  const store = createStore();
  
  return (
    <Provider store={store}>
      <YourApp />
    </Provider>
  );
}
```

### Transactions & Batching

#### `transaction<T>(fn: () => T, store?: Store): T`
Executes a function within a transaction, rolling back on error.

```tsx
const result = transaction(() => {
  store.set(userAtom, newUser);
  store.set(sessionAtom, newSession);
  return 'success';
}, store);
```

#### `txn(store?: Store): Transaction`
Creates a manual transaction with `commit()` and `rollback()` methods.

```tsx
const txn = txn(store);
try {
  store.set(userAtom, user);
  txn.commit();
} catch (error) {
  txn.rollback();
  throw error;
}
```

#### `useTransaction<Args, Return>(fn: (...) => Return, deps?: React.DependencyList): (...) => Return`
Hook that wraps a function in transactions.

```tsx
const updateUser = useTransaction((userId: string, data: UserData) => {
  store.set(currentUserIdAtom, userId);
  store.set(userDataAtom, data);
});
```

#### `batch(fn: () => void, store?: Store): void`
Batches updates for the next render cycle.

```tsx
batch(() => {
  setUser({ name: 'John' });
  setAge(25);
});
```

#### `useBatch(): (fn: () => void) => void`
Hook returning a batched function.

```tsx
const batchedUpdate = useBatch();
batchedUpdate(() => {
  setUser(newUser);
  setSettings(newSettings);
});
```

#### `batchAtomUpdates<T>(updates: Array<{ atom: any; value: any }>, store?: Store): T | void`
Batches multiple atom updates atomically.

```tsx
batchAtomUpdates([
  { atom: userAtom, value: newUser },
  { atom: sessionAtom, value: newSession },
]);
```

#### `transactionWithRetry<T>(fn: () => T | Promise<T>, options?: RetryOptions): Promise<T>`
Transaction with automatic retry on failure.

```tsx
const result = await transactionWithRetry(
  async () => {
    await syncUserData(userId);
    return 'synced';
  },
  { maxRetries: 3, retryDelay: 1000 }
);
```

### Actions

#### `atomWithActions<T, A>(config: ActionConfig<T, A>): AtomWithActionsResult<T, A>`
Creates an atom with associated actions.

```tsx
const userActions = atomWithActions({
  default: null,
  actions: {
    load: async (get, set, userId: string) => {
      const user = await api.getUser(userId);
      set(userActions.atom, user);
      return user;
    },
    update: async (get, set, data: Partial<User>) => {
      const user = await api.updateUser(get(userActions.atom).id, data);
      set(userActions.atom, user);
      return user;
    }
  },
  options: {
    onOptimisticUpdate: (current, actionName, args) => {
      if (actionName === 'update') {
        return { ...current, ...args[0] };
      }
      return current;
    },
    onError: (error, actionName, args, currentValue) => {
      console.error('Action failed:', actionName, error);
      return currentValue; // Keep current value on error
    }
  }
});

// Usage
const [user] = useAtom(userActions.atom);
await userActions.actions.load('123');
await userActions.actions.update({ name: 'John' });
```

#### `advancedAtomWithActions<T, A>(config: ActionConfig<T, A>): AdvancedAtomWithActionsResult<T, A>`
Includes loading and error state atoms that are automatically wired to the internal action state.

```tsx
const advanced = advancedAtomWithActions({
  default: null,
  actions: { 
    load: async (get, set, id: string) => {
      const user = await api.getUser(id);
      set(advanced.atom, user);
      return user;
    }
  }
});

// These atoms automatically reflect the real action state
const [isLoading] = useAtom(advanced.isLoadingAtom);
const [error] = useAtom(advanced.errorAtom);
const [pendingActions] = useAtom(advanced.pendingActionsAtom);

// isLoading will be true while load action is running
// error will contain any thrown errors
// pendingActions will show the set of running action IDs
```

#### Action Helpers

```tsx
// Async actions
const asyncLoad = createAsyncAction(async (userId: string) => {
  return await api.getUser(userId);
});

// Reducer actions
const countAtom = atom(0);
const incrementBy = createReducerAction(countAtom,
  (current: number, amount: number) => current + amount
);

// Retry actions
const retryLoad = createRetryAction(asyncLoad, {
  maxRetries: 3,
  retryDelay: 1000,
  shouldRetry: (error) => error.message.includes('timeout')
});
```

### Queue

#### `queueAtom<T>(initialValue: T, options?: QueueOptions<T>): QueueAtomResult<T>`
Creates a queue atom for FIFO operation processing.

```tsx
const dataQueue = queueAtom(null, {
  onError: (error, previousValue) => {
    console.error('Queue operation failed:', error);
    return previousValue; // Recovery value
  }
});

const [data] = useAtom(dataQueue.state);
const [status] = useAtom(dataQueue.statusAtom); // 'idle' | 'running' | 'error'

await dataQueue.enqueue(async (current) => {
  const newData = await fetchData();
  return newData;
});

dataQueue.clear(); // Clears queue and resets value
const queueLength = dataQueue.getQueueLength();
```

#### `enhancedQueueAtom<T>(initialValue: T, options?: EnhancedQueueOptions<T>): QueueAtomResult<T> & Enhancements`
Extended queue with additional features.

```tsx
const enhanced = enhancedQueueAtom(null, {
  maxConcurrency: 2,
  timeout: 5000,
  retryOptions: { maxRetries: 2, retryDelay: 500 },
  onError: (error, prevValue) => prevValue
});

const [isPaused] = useAtom(enhanced.isPaused);
enhanced.pause();
enhanced.resume();

await enhanced.enqueue(async (current) => {
  await processData(current);
});
```

### Computed Helpers

#### `computed<Deps, Result>(deps: Deps, computeFn: ComputedFn<Deps, Result>, debugName?: string): Atom<Result>`
Creates a computed atom from multiple dependencies.

```tsx
const fullName = computed(
  [firstNameAtom, lastNameAtom],
  ([first, last]) => `${first} ${last}`
);
```

#### `memoizedComputed<Deps, Result>(deps: Deps, computeFn: ComputedFn<Deps, Result>, options?: MemoOptions<Result>): Atom<Result>`
Memoized computed with custom equality (defaults to `Object.is`).

```tsx
const expensive = memoizedComputed(
  [dataAtom],
  ([data]) => heavyComputation(data),
  { 
    isEqual: (a: b) => JSON.stringify(a) === JSON.stringify(b)
  }
);
```

#### `selectComputed<Source, Selected>(sourceAtom: Atom<Source>, selector: (value: Source) => Selected, options?: SelectOptions<Selected>): Atom<Selected>`
Computed that only updates when the selector result changes.

```tsx
const userName = selectComputed(
  userAtom,
  (user) => user.name,
  { isEqual: (a, b) => a === b }
);
```

#### `asyncComputed<Deps, Result>(deps: Deps, computeFn: AsyncComputedFn<Deps, Result>, options?: AsyncOptions<Result>): Atom<Result | Promise<Result>>`
Computed with async support.

```tsx
const searchResults = asyncComputed(
  [queryAtom],
  async ([query]) => {
    const results = await search(query);
    return results;
  },
  { defaultValue: [] }
);
```

#### Array Helpers

```tsx
const users = atom([{ id: 1, name: 'John', age: 25 }]);

const adults = filterComputed(users, (user) => user.age >= 18);
const names = mapComputed(users, (user) => user.name);
const totalAge = reduceComputed(users, (acc, user) => acc + user.age, 0);

const combined = combineComputed({
  users,
  adults,
  names
});
```

### Focus/Lens Utilities

#### `focusAtom<Source, Target>(baseAtom: WritableAtom<Source>, optic: Optic<Source: Target>): WritableAtom<Target>`
Creates a focused atom using an optic lens.

```tsx
const userAtom = atom({ 
  profile: { 
    settings: { theme: 'dark' } 
  } 
});

const themeAtom = focusAtom(userAtom, 
  compose(
    prop('profile'),
    compose(compose(prop('settings'), prop('theme'))
  )
);

const [theme: setTheme] = useAtom(themeAtom);
setTheme('light'); // Updates nested structure
```

#### Optics

```tsx
// Property lens
const emailAtom = focusAtom(userAtom, prop('email'));

// Index lens  
const firstItemAtom = focusAtom(arrayAtom, index(0));

// Path lens
const settingsAtom = focusAtom(userAtom, path('profile', 'settings'));

// Composition
const userNameAtom = focusAtom(
  usersAtom, 
  compose(
    compose(index(0), prop('name'))
  )
);

// Value transformation
const themeDisplayAtom = focusAtom(
  themeAtom,
  transform(
    prop('theme'),
    {
      to: (theme: string) => theme.toUpperCase(),
      from: (display: string) => display.toLowerCase()
    }
  )
);

// Conditional focusing
const currentValueAtom = focusAtom(
  dataAtom,
  conditional(
    (data) => data.type === 'user',
    prop('userValue'),
    prop('defaultValue')
  )
);

// Safe focusing with default
const safeNestedAtom = safeFocusAtom(
  configAtom,
  path('nested', 'value' as any),
  'default'
);

// Split atom into multiple focused parts
const { name, age, email } = splitAtom(userAtom, {
  name: prop('name'),
  age: prop('age'), 
  email: prop('email')
});
```

## Usage Examples

### Basic Counter

```tsx
import { atom, useAtom } from '@react-sky/atom';

const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  
  return (
    <div>
      <button onClick={() => setCount(count - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}
```

### Derived Values

```tsx
const firstName = atom('John');
const lastName = atom('Doe');
const fullName = atom((get) => `${get(firstName)} ${get(lastName)}`);

// Writable derived atom
const displayName = atom(
  (get) => get(fullName).toUpperCase(),
  (get, set, value: string) => {
    const parts = value.toLowerCase().split(' ');
    set(firstName, parts[0]);
    set(lastName, parts[1]);
  }
);
```

### Async Atoms

```tsx
const userIdAtom = atom(1);
const userAtom = atom(async (get) => {
  const id = get(userIdAtom);
  const user = await fetchUser(id);
  return user;
});

function UserProfile() {
  // With Suspense (default)
  const user = useAtomValue(userAtom);
  
  // Or without Suspense
  const state = useAtomValue(userAtom, { suspense: false });
  if (state.loading) return <Spinner />;
  if (state.error) return <ErrorMessage />;
  
  return <div>{state.data.name}</div>;
}
```

### Store Provider & Snapshots

```tsx
function App() {
  const store = createStore();
  
  return (
    <Provider store={store}>
      <UserApp />
    </Provider>
  );
}

// SSR example
// Server: 
const serverStore = createStore();
const snapshot = serverStore.snapshot();
// Pass snapshot to client

// Client:
const clientStore = createStore({ snapshot });
```

### Transactions & Batching

```tsx
import { transaction, batch } from '@react-sky/atom';

const userAtom = atom(null);
const sessionAtom = atom(null);

// Atomic transaction
transaction(() => {
  store.set(userAtom, user);
  store.set(sessionAtom, session);
});

// Batched updates
batch(() => {
  setUser(newUser);
  setSettings(newSettings);
});
```

### Actions with Optimistic Updates

```tsx
const todosActions = atomWithActions({
  default: [],
  actions: {
    add: async (get, set, text: string) => {
      const newTodo = await api.createTodo(text);
      set(todosActions.atom, [...get(todosActions.atom), newTodo]);
      return newTodo;
    },
    toggle: async (get, set, id: string) => {
      await api.toggleTodo(id);
      const updated = get(todosActions.atom).map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      );
      set(todosActions.atom, updated);
    }
  },
  options: {
    onOptimisticUpdate: (current, actionName, args) => {
      if (actionName === 'add') {
        return [...current, { id: Date.now(), text: args[0], completed: false }];
      }
      return current;
    }
  }
});
```

### Queue Processing

```tsx
const documentsQueue = queueAtom([], {
  onError: (error, previousValue) => previousValue
});

await documentsQueue.enqueue(async (docs) => {
  const processed = await processDocuments(docs);
  return processed;
});
```

### Computed with Memoization

```tsx
const items = atom([{ id: 1, price: 10 }, { id: 2, price: 20 }]);

const totalPrice = memoizedComputed([items], ([items]) => 
  items.reduce((sum: item) => sum + item.price, 0)
);

const expensiveItems = filterComputed(items, item => item.price > 15);
```

### Focused State Editing

```tsx
const configAtom = atom({
  user: { email: 'test@example.com' },
  theme: 'dark'
});

const emailAtom = focusAtom(configAtom, path('user', 'email'));
const themeAtom = focusAtom(configAtom, prop('theme'));

// Split into multiple atoms
const { email: theme } = splitAtom(configAtom, {
  email: path('user', 'email'),
  theme: prop('theme')
});
```

## Best Practices

1. **Keep atoms small and focused**: Each atom should represent a single piece of state
2. **Prefer derived atoms**: Use computed atoms over useEffect for derived state
3. **Batch related updates**: Use `transaction()` or `batch()` to minimize re-renders
4. **Handle async gracefully**: Use Suspense for loading states or `suspense: false` for manual handling
5. **Use TypeScript**: Leverage the library's strong typing for better DX
6. **Leverage focus utilities**: Use `focusAtom` and `splitAtom` for nested state management
7. **Error boundaries**: Wrap async atoms in error boundaries for graceful failure handling

### SSR Guidelines for Next.js App Router

```tsx
// app/layout.tsx
export default function RootLayout() {
  const store = createStore();
  // Populate server-side state
  
  return (
    <html>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `window.__ATOM_STATE__ = ${JSON.stringify(store.snapshot())}`
          }}
        />
      </head>
      <body>
        <Provider store={store}>
          {children}
        </Provider>
      </body>
    </html>
  );
}

// app/client-wrapper.tsx
'use client';
import { createStore } from '@react-sky/atom';

const initialState = JSON.parse(window.__ATOM_STATE__);
const clientStore = createStore({ snapshot: initialState });

export default function ClientWrapper({ children }) {
  return (
    <Provider store={clientStore}>
      {children}
    </Provider>
  );
}
```

## FAQ

**Q: How does this compare to global stores like Redux?**
A: Atom-based state is naturally granular and composable. Dependencies are automatic, subscriptions are fine-grained, and there's no single global store to manage.

**Q: What about async atoms throwing Promises?**
A: Promises are thrown to work with React's Suspense. Use `<Suspense>` boundaries or `suspense: false` to handle loading states manually.

**Q: Can I use this with Next.js SSR?**
A: Yes! See actions examples or SSR guidelines. Capture state with `snapshot()` on the server and restore with `createStore({ snapshot })` on the client.