atom-state

from front-depiction/claude-setup

Reusable Claude Code configuration for Effect TypeScript projects with specialized agents and skills

10 stars4 forksUpdated Jan 19, 2026
npx skills add https://github.com/front-depiction/claude-setup --skill atom-state

SKILL.md

Effect Atom State Management

Effect Atom is a reactive state management library for Effect that seamlessly integrates with React.

Core Concepts

Atoms as References

Atoms work by reference - they are stable containers for reactive state:

import * as Atom from "@effect-atom/atom-react"

// Atoms are created once and referenced throughout the app
export const counterAtom = Atom.make(0)

// Multiple components can reference the same atom
// All update when the atom value changes

Automatic Cleanup

Atoms automatically reset when no subscribers remain (unless marked with keepAlive):

// Resets when last subscriber unmounts
export const temporaryState = Atom.make(initialValue)

// Persists across component lifecycles
export const persistentState = Atom.make(initialValue).pipe(Atom.keepAlive)

Lazy Evaluation

Atom values are computed on-demand when subscribers access them.

Pattern: Basic Atoms

import * as Atom from "@effect-atom/atom-react"

// Simple atom
export const count = Atom.make(0)

// Atom with object state
export interface CartState {
  readonly items: ReadonlyArray<Item>
  readonly total: number
}

export const cart = Atom.make<CartState>({
  items: [],
  total: 0
})

Pattern: Derived Atoms

Use Atom.map or computed atoms with the get parameter:

// Derived via map
export const itemCount = Atom.map(cart, (c) => c.items.length)
export const isEmpty = Atom.map(cart, (c) => c.items.length === 0)

// Computed atom accessing other atoms
export const cartSummary = Atom.make((get) => {
  const cartData = get(cart)
  const count = get(itemCount)

  return {
    itemCount: count,
    total: cartData.total,
    isEmpty: count === 0
  }
})

Pattern: Atom Family (Dynamic Atoms)

Use Atom.family for stable references to dynamically created atoms:

// Create atoms per entity ID
export const userAtoms = Atom.family((userId: string) =>
  Atom.make<User | null>(null).pipe(Atom.keepAlive)
)

// Usage - always returns the same atom for a given ID
const userAtom = userAtoms(userId)

Pattern: Atom.fn for Async Actions

Use Atom.fn with Effect.fnUntraced for async operations:

  • Reading gives Result<Success, Error> with automatic .waiting flag
  • Triggering via useAtomSet runs the effect
import { Atom, useAtomValue, useAtomSet } from "@effect-atom/atom-react"
import { Effect, Exit } from "effect"

// Atom.fn with Effect.fnUntraced for generator syntax
const logAtom = Atom.fn(
  Effect.fnUntraced(function* (arg: number) {
    yield* Effect.log("got arg", arg)
  })
)

function LogComponent() {
  // useAtomSet returns a trigger function
  const logNumber = useAtomSet(logAtom)
  return <button onClick={() => logNumber(42)}>Log 42</button>
}

With services using Atom.runtime:

class Users extends Effect.Service<Users>()("app/Users", {
  effect: Effect.gen(function* () {
    const create = (name: string) => Effect.succeed({ id: 1, name })
    return { create } as const
  }),
}) {}

const runtimeAtom = Atom.runtime(Users.Default)

// runtimeAtom.fn provides service access
const createUserAtom = runtimeAtom.fn(
  Effect.fnUntraced(function* (name: string) {
    const users = yield* Users
    return yield* users.create(name)
  })
)

function CreateUserComponent() {
  // mode: "promiseExit" for async handlers with Exit result
  const createUser = useAtomSet(createUserAtom, { mode: "promiseExit" })
  return (
    <button onClick={async () => {
      const exit = await createUser("John")
      if (Exit.isSuccess(exit)) {
        console.log(exit.value)
      }
    }}>
      Create user
    </button>
  )
}

Reading result state:

function UserList() {
  const [result, createUser] = useAtom(createUserAtom)  // Result<User, Error>

  // Use matchWithWaiting for proper waiting state handling
  return Result.matchWithWaiting(result, {
    onWaiting: () => <Spinner />,
    onSuccess: ({ value }) => <UserCard user={value} />,
    onError: (error) => <Error message={String(error)} />,
    onDefect: (defect) => <Error message={String(defect)} />
  })
}

Anti-pattern: Manual void wrappers

// ❌ DON'T - manual state management loses waiting control
const loading$ = Atom.make(false)
const user$ = Atom.make<User | null>(null)

const fetchUser = (id: string): void => {
  registry.set(loading$, true)
  Effect.runPromise(userService.getById(id)).then(user => {
    registry.set(user$, user)
    registry.set(loading$, false)
  })
}

// ✅ DO - Atom.fn handles loading/success/failure automatically
const fetchUserAtom = Atom.fn(
  Effect.fnUntraced(function* (id: string) {
    return yield* userService.getById(id)
  })
)
// result.waiting, Result.match - all built-in

Pattern: Runtime with Services

Wrap Effect layers/services for use in atoms:

import { Layer } from "effect"

// Create runtime with services
expo

...
Read full content

Repository Stats

Stars10
Forks4