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

Claude Code for Jotai Advanced: Atomic State Management Patterns

Published: June 9, 2027
Read time: 6 min read
By: Claude Skills 360

Jotai advanced patterns go beyond basic atoms — atomWithStorage(key, initialValue) persists to localStorage with automatic sync. atomWithQuery((get) => ({ queryKey, queryFn })) integrates React Query into Jotai. atom((get, { signal }) => fetch(url, { signal })) builds abort-safe async atoms. atomWithReset(initialValue) returns a [atom, resetAtom] pair. atomFamily((id) => atom(fetchPost(id))) creates parameterized atoms. loadable(asyncAtom) converts a suspenseful atom to { state: "loading" | "hasData" | "hasError" }. selectAtom(sourceAtom, (v) => v.slice(0, 5)) creates cheap derived atoms. atom(null, (get, set, update) => { set(countAtom, get(countAtom) + update) }) creates write-only atoms. onMount: (set) => { const unsub = ws.subscribe(...); return unsub } runs effects. splitAtom(listAtom) enables efficient per-item atoms. Provider with store creates scoped atom stores. Claude Code generates Jotai atom stores, async query atoms, persistence patterns, and complex derived state.

CLAUDE.md for Jotai Advanced

## Jotai Advanced Stack
- Version: jotai >= 2.9, jotai-tanstack-query >= 0.8 (for atomWithQuery)
- Storage: import { atomWithStorage } from "jotai/utils" — syncs to localStorage automatically
- Async: atom(async (get) => { const id = get(userIdAtom); return fetchUser(id) }) — use with Suspense
- Loadable: import { loadable } from "jotai/utils"; const loadable = loadable(asyncAtom) — no Suspense needed
- Family: import { atomFamily } from "jotai/utils"; const postAtom = atomFamily((id: string) => atom(fetchPost(id)))
- Reset: import { atomWithReset, useResetAtom } from "jotai/utils"
- Select: import { selectAtom } from "jotai/utils"; const nameAtom = selectAtom(userAtom, u => u.name)
- Split: import { splitAtom } from "jotai/utils"; const itemAtomsAtom = splitAtom(itemsAtom)

Core Atom Definitions

// store/atoms.ts — Jotai atom store
import { atom } from "jotai"
import {
  atomWithStorage,
  atomWithReset,
  atomFamily,
  loadable,
  selectAtom,
  splitAtom,
  atomWithRefresh,
} from "jotai/utils"
import { focusAtom } from "jotai-optics"

// ── Auth atoms ─────────────────────────────────────────────────────────────

export type AuthUser = {
  id: string
  email: string
  name: string
  role: "user" | "admin"
  avatarUrl: string | null
}

// Persisted across page reloads
export const authTokenAtom = atomWithStorage<string | null>("auth_token", null)

// Derived: is user authenticated?
export const isAuthenticatedAtom = atom((get) => get(authTokenAtom) !== null)

// User profile — loaded based on token
export const currentUserAtom = atom(async (get) => {
  const token = get(authTokenAtom)
  if (!token) return null

  const res = await fetch("/api/users/me", {
    headers: { Authorization: `Bearer ${token}` },
  })

  if (!res.ok) return null
  return res.json() as Promise<AuthUser>
})

// Loadable version for components that can't use Suspense
export const currentUserLoadableAtom = loadable(currentUserAtom)

// Select sub-fields without re-rendering on unrelated changes
export const userRoleAtom = selectAtom(
  currentUserAtom,
  (user) => user?.role ?? null,
)

// ── Cart atoms ─────────────────────────────────────────────────────────────

export type CartItem = {
  id: string
  productId: string
  name: string
  price: number
  quantity: number
  imageUrl: string | null
}

export const cartItemsAtom = atomWithStorage<CartItem[]>("cart_v2", [])

// Derived cart stats
export const cartCountAtom = selectAtom(
  cartItemsAtom,
  (items) => items.reduce((sum, item) => sum + item.quantity, 0),
)

export const cartTotalAtom = selectAtom(
  cartItemsAtom,
  (items) => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
)

// Write atom: add to cart
export const addToCartAtom = atom(null, (get, set, item: CartItem) => {
  const items = get(cartItemsAtom)
  const existing = items.find(i => i.productId === item.productId)

  if (existing) {
    set(cartItemsAtom, items.map(i =>
      i.productId === item.productId
        ? { ...i, quantity: i.quantity + item.quantity }
        : i,
    ))
  } else {
    set(cartItemsAtom, [...items, item])
  }
})

// Write atom: remove from cart
export const removeFromCartAtom = atom(null, (get, set, productId: string) => {
  set(cartItemsAtom, get(cartItemsAtom).filter(i => i.productId !== productId))
})

// Write atom: update quantity
export const updateQuantityAtom = atom(
  null,
  (get, set, { productId, quantity }: { productId: string; quantity: number }) => {
    if (quantity <= 0) {
      set(cartItemsAtom, get(cartItemsAtom).filter(i => i.productId !== productId))
    } else {
      set(cartItemsAtom, get(cartItemsAtom).map(i =>
        i.productId === productId ? { ...i, quantity } : i,
      ))
    }
  },
)

// Resettable atom for empty cart
export const [cartAtom, resetCartAtom] = atomWithReset<CartItem[]>([])

// ── Per-item atoms with atomFamily ─────────────────────────────────────────

export type Post = {
  id: string
  title: string
  content: string
  liked: boolean
  bookmarked: boolean
}

// One atom per post ID — cached automatically
export const postAtomFamily = atomFamily((postId: string) =>
  atom(async () => {
    const res = await fetch(`/api/posts/${postId}`)
    if (!res.ok) throw new Error("Post not found")
    return res.json() as Promise<Post>
  }),
)

// Search state with debounced fetch
export const searchQueryAtom = atom("")
export const searchResultsAtom = atom(async (get) => {
  const query = get(searchQueryAtom)
  if (query.length < 2) return []

  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
  return res.json() as Promise<Post[]>
})

export const searchResultsLoadableAtom = loadable(searchResultsAtom)

// ── Settings with nested focus ─────────────────────────────────────────────

type AppSettings = {
  theme: "light" | "dark" | "system"
  language: string
  notifications: {
    email: boolean
    push: boolean
    sms: boolean
  }
  sidebar: {
    collapsed: boolean
    width: number
  }
}

export const settingsAtom = atomWithStorage<AppSettings>("app_settings_v2", {
  theme: "system",
  language: "en",
  notifications: { email: true, push: true, sms: false },
  sidebar: { collapsed: false, width: 280 },
})

// Focus atoms for nested sub-trees
export const themeAtom = focusAtom(settingsAtom, (o) => o.prop("theme"))
export const notificationsAtom = focusAtom(settingsAtom, (o) => o.prop("notifications"))
export const sidebarAtom = focusAtom(settingsAtom, (o) => o.prop("sidebar"))

React Components with Jotai Hooks

// components/cart/CartDrawer.tsx — Jotai cart with split atoms
"use client"
import {
  useAtom,
  useAtomValue,
  useSetAtom,
} from "jotai"
import {
  cartItemsAtom,
  cartCountAtom,
  cartTotalAtom,
  addToCartAtom,
  removeFromCartAtom,
  updateQuantityAtom,
  resetCartAtom,
  type CartItem,
} from "@/store/atoms"
import { splitAtom } from "jotai/utils"
import { useMemo } from "react"

// Split list atom into per-item atoms for efficient rendering
const cartItemAtomsAtom = splitAtom(cartItemsAtom)

function CartItemRow({ itemAtom }: { itemAtom: ReturnType<typeof splitAtom<CartItem>>[number] }) {
  const [item] = useAtom(itemAtom)
  const updateQuantity = useSetAtom(updateQuantityAtom)
  const removeFromCart = useSetAtom(removeFromCartAtom)

  return (
    <div className="flex items-center gap-3 py-3">
      {item.imageUrl && (
        <img src={item.imageUrl} alt={item.name} className="size-16 rounded-lg object-cover flex-shrink-0" />
      )}
      <div className="flex-1 min-w-0">
        <p className="font-medium text-sm truncate">{item.name}</p>
        <p className="text-sm text-muted-foreground">${(item.price / 100).toFixed(2)}</p>
      </div>
      <div className="flex items-center gap-2">
        <button onClick={() => updateQuantity({ productId: item.productId, quantity: item.quantity - 1 })}
          className="size-7 rounded border flex items-center justify-center text-sm">

        </button>
        <span className="w-8 text-center text-sm">{item.quantity}</span>
        <button onClick={() => updateQuantity({ productId: item.productId, quantity: item.quantity + 1 })}
          className="size-7 rounded border flex items-center justify-center text-sm">
          +
        </button>
      </div>
      <button onClick={() => removeFromCart(item.productId)}
        className="text-sm text-destructive hover:underline ml-2">
        Remove
      </button>
    </div>
  )
}

export function CartDrawer({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
  const itemAtoms = useAtomValue(cartItemAtomsAtom)
  const count = useAtomValue(cartCountAtom)
  const total = useAtomValue(cartTotalAtom)
  const resetCart = useSetAtom(resetCartAtom)

  if (!isOpen) return null

  return (
    <div className="fixed inset-0 z-50 flex justify-end">
      <div className="w-96 bg-background border-l shadow-xl flex flex-col h-full">
        <div className="flex items-center justify-between p-4 border-b">
          <h2 className="font-semibold">Cart ({count})</h2>
          <button onClick={onClose}>✕</button>
        </div>

        <div className="flex-1 overflow-y-auto px-4 divide-y">
          {itemAtoms.map((itemAtom) => (
            <CartItemRow key={`${itemAtom}`} itemAtom={itemAtom} />
          ))}
          {itemAtoms.length === 0 && (
            <p className="text-center text-muted-foreground py-12">Your cart is empty</p>
          )}
        </div>

        {itemAtoms.length > 0 && (
          <div className="p-4 border-t space-y-3">
            <div className="flex justify-between font-semibold">
              <span>Total</span>
              <span>${(total / 100).toFixed(2)}</span>
            </div>
            <button className="w-full py-3 bg-primary text-primary-foreground rounded-xl font-medium">
              Checkout
            </button>
            <button onClick={() => resetCart()} className="w-full text-sm text-muted-foreground hover:underline">
              Clear cart
            </button>
          </div>
        )}
      </div>
    </div>
  )
}

For the Zustand alternative when a simpler store-based API (not atomic) with create() and middleware like immer, devtools, and persist is preferred — Zustand has a flatter learning curve and is better for large stores where you want to keep related state together, while Jotai excels at granular invalidation where components re-render only when their specific atoms change, see the Zustand advanced guide. For the Recoil alternative when Facebook’s atomic model with selector dependency tracking, selectorFamily, and atomEffects for cross-cutting concerns across atoms is familiar — Recoil and Jotai have similar philosophies but Jotai has less boilerplate and better TypeScript support, see the Recoil guide. The Claude Skills 360 bundle includes Jotai advanced skill sets covering storage, async atoms, and derived state. Start with the free tier to try atomic state management 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