Claude Code for Jotai: Atomic State Management for React — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Jotai: Atomic State Management for React
Frontend

Claude Code for Jotai: Atomic State Management for React

Published: March 11, 2027
Read time: 8 min read
By: Claude Skills 360

Jotai manages React state as independent atoms — components subscribe only to the atoms they use, so unrelated state changes never trigger re-renders. atom(initialValue) creates a primitive atom; atom(get => computation) creates a derived read-only atom; atom(get, (get, set) => setter) creates a writable derived atom. useAtom returns [value, setter]; useAtomValue for read-only; useSetAtom for write-only. atomWithStorage syncs an atom to localStorage or sessionStorage. atomWithReducer implements the reducer pattern for event-driven state. atomFamily creates parameterized atoms indexed by a key — perfect for per-entity state. atomWithQuery from jotai-tanstack-query integrates async data fetching. createStore runs Jotai outside React for testing or SSR. Claude Code generates Jotai atom definitions, derived atoms, async patterns, and the atom family configurations for fine-grained subscription in React applications.

CLAUDE.md for Jotai

## Jotai Stack
- Version: jotai >= 2.10, jotai-devtools >= 0.10
- Primitive: const cartAtom = atom<CartItem[]>([])
- Derived read: const totalAtom = atom(get => get(cartAtom).reduce(...))
- Writable derived: atom(get, (get, set, val) => { set(baseAtom, transform(val)) })
- Async: atom(async get => { const id = get(userIdAtom); return fetch(..) }) — suspense
- Storage: atomWithStorage("key", defaultValue) — localStorage/sessionStorage
- Family: atomFamily(id => atom(async get => fetchOrder(id))) — per-ID atoms
- Integration: jotai-tanstack-query — atomWithQuery, atomWithInfiniteQuery
- SSR: const store = createStore() — hydrate before render

Atom Definitions

// atoms/order-atoms.ts — atomic state for orders
import { atom } from "jotai"
import { atomWithStorage, atomWithReducer } from "jotai/utils"
import { atomFamily } from "jotai/utils"

// Auth atoms
export const userIdAtom = atom<string | null>(null)
export const isAuthenticatedAtom = atom(get => get(userIdAtom) !== null)

// Orders list — filters
export const statusFilterAtom = atom<"all" | "pending" | "shipped" | "delivered">("all")
export const searchQueryAtom = atom("")
export const pageAtom = atom(1)

// Reset page when filter changes
export const statusFilterWithResetAtom = atom(
  get => get(statusFilterAtom),
  (_get, set, newStatus: typeof statusFilterAtom extends ReturnType<typeof atom<infer T>> ? T : never) => {
    set(statusFilterAtom, newStatus)
    set(pageAtom, 1)  // Reset to page 1 on filter change
  }
)

// Cart atoms
interface CartItem {
  productId: string
  name: string
  priceCents: number
  quantity: number
}

export const cartItemsAtom = atomWithStorage<CartItem[]>("cart-items", [])

// Derived cart totals
export const cartTotalAtom = atom(get =>
  get(cartItemsAtom).reduce((sum, item) => sum + item.priceCents * item.quantity, 0)
)

export const cartCountAtom = atom(get =>
  get(cartItemsAtom).reduce((sum, item) => sum + item.quantity, 0)
)

// Writable derived: add item with quantity merge
export const addToCartAtom = atom(
  null,
  (get, set, item: Omit<CartItem, "quantity">) => {
    set(cartItemsAtom, currentItems => {
      const existing = currentItems.find(i => i.productId === item.productId)
      if (existing) {
        return currentItems.map(i =>
          i.productId === item.productId
            ? { ...i, quantity: i.quantity + 1 }
            : i
        )
      }
      return [...currentItems, { ...item, quantity: 1 }]
    })
  }
)

export const removeFromCartAtom = atom(
  null,
  (_get, set, productId: string) => {
    set(cartItemsAtom, items => items.filter(i => i.productId !== productId))
  }
)

export const clearCartAtom = atom(null, (_get, set) => {
  set(cartItemsAtom, [])
})

Async Atoms with Suspense

// atoms/async-atoms.ts — async data fetching
import { atom } from "jotai"
import { userIdAtom, statusFilterAtom, pageAtom, searchQueryAtom } from "./order-atoms"

interface Order {
  id: string
  status: string
  totalCents: number
  createdAt: string
}

// Async atom — suspends while loading
export const ordersAtom = atom(async get => {
  const userId = get(userIdAtom)
  if (!userId) return { orders: [], totalCount: 0 }

  const status = get(statusFilterAtom)
  const search = get(searchQueryAtom)
  const page = get(pageAtom)

  const params = new URLSearchParams({
    customerId: userId,
    page: String(page),
    limit: "20",
    ...(status !== "all" && { status }),
    ...(search && { search }),
  })

  const response = await fetch(`/api/orders?${params}`)
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json() as Promise<{ orders: Order[]; totalCount: number }>
})

// Single order by ID — per-entity with atomFamily
export const orderAtomFamily = atomFamily((orderId: string) =>
  atom(async () => {
    const response = await fetch(`/api/orders/${orderId}`)
    if (!response.ok) throw new Error(`Order ${orderId} not found`)
    return response.json() as Promise<Order>
  })
)

atomWithReducer

// atoms/checkout-atom.ts — reducer pattern for checkout state
import { atomWithReducer } from "jotai/utils"

type CheckoutState =
  | { step: "cart" }
  | { step: "shipping"; cartTotal: number }
  | { step: "payment"; shippingAddress: Address; cartTotal: number }
  | { step: "confirming"; paymentMethodId: string; shippingAddress: Address; cartTotal: number }
  | { step: "success"; orderId: string }
  | { step: "error"; message: string }

type CheckoutAction =
  | { type: "PROCEED_TO_SHIPPING"; cartTotal: number }
  | { type: "ADD_SHIPPING_ADDRESS"; address: Address }
  | { type: "ADD_PAYMENT_METHOD"; paymentMethodId: string }
  | { type: "ORDER_CONFIRMED"; orderId: string }
  | { type: "ERROR"; message: string }
  | { type: "BACK" }
  | { type: "RESET" }

function checkoutReducer(state: CheckoutState, action: CheckoutAction): CheckoutState {
  switch (action.type) {
    case "PROCEED_TO_SHIPPING":
      return { step: "shipping", cartTotal: action.cartTotal }
    case "ADD_SHIPPING_ADDRESS":
      if (state.step !== "shipping") return state
      return { step: "payment", shippingAddress: action.address, cartTotal: state.cartTotal }
    case "ADD_PAYMENT_METHOD":
      if (state.step !== "payment") return state
      return {
        step: "confirming",
        paymentMethodId: action.paymentMethodId,
        shippingAddress: state.shippingAddress,
        cartTotal: state.cartTotal,
      }
    case "ORDER_CONFIRMED":
      return { step: "success", orderId: action.orderId }
    case "ERROR":
      return { step: "error", message: action.message }
    case "BACK":
      if (state.step === "payment") return { step: "shipping", cartTotal: state.cartTotal }
      if (state.step === "shipping") return { step: "cart" }
      return { step: "cart" }
    case "RESET":
      return { step: "cart" }
    default:
      return state
  }
}

export const checkoutAtom = atomWithReducer<CheckoutState, CheckoutAction>(
  { step: "cart" },
  checkoutReducer
)

React Components

// components/CartButton.tsx — write-only atom subscription
import { useSetAtom, useAtomValue } from "jotai"
import { addToCartAtom, cartCountAtom } from "@/atoms/order-atoms"

export function AddToCartButton({ product }: { product: Product }) {
  const addToCart = useSetAtom(addToCartAtom)  // Only subscribes to setter — no re-renders on count change
  const cartCount = useAtomValue(cartCountAtom)

  return (
    <button
      onClick={() => addToCart({
        productId: product.id,
        name: product.name,
        priceCents: product.priceCents,
      })}
      className="btn btn-primary"
    >
      Add to Cart {cartCount > 0 && <span className="badge">{cartCount}</span>}
    </button>
  )
}

// components/OrdersPage.tsx — async atom with Suspense
import { Suspense } from "react"
import { useAtomValue, useSetAtom } from "jotai"
import { ordersAtom, statusFilterWithResetAtom } from "@/atoms"

function OrdersList() {
  // Suspends until ordersAtom resolves
  const { orders, totalCount } = useAtomValue(ordersAtom)
  const setStatus = useSetAtom(statusFilterWithResetAtom)

  return (
    <div>
      <p>{totalCount} orders</p>
      {orders.map(order => <OrderCard key={order.id} order={order} />)}
    </div>
  )
}

export function OrdersPage() {
  return (
    <ErrorBoundary fallback={<p>Failed to load orders</p>}>
      <Suspense fallback={<OrdersSkeleton />}>
        <OrdersList />
      </Suspense>
    </ErrorBoundary>
  )
}

For the Zustand alternative that uses a single store with slices rather than individual atoms — better for teams preferring one centralized state store with explicit action functions, or when state needs to be accessed outside React components, see the Zustand Advanced guide for the slice and middleware patterns. For the TanStack Query alternative when the majority of state is server state (asynchronous fetched data) rather than client-side application state — Jotai works well alongside TanStack Query with atoms holding only UI/client state, see the TanStack Query Advanced guide for server state patterns. The Claude Skills 360 bundle includes Jotai skill sets covering atoms, derived state, and async patterns. Start with the free tier to try Jotai atom generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free