Claude Code for Recoil: Facebook's Atom-Based State Management — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Recoil: Facebook's Atom-Based State Management
Frontend

Claude Code for Recoil: Facebook's Atom-Based State Management

Published: May 7, 2027
Read time: 6 min read
By: Claude Skills 360

Recoil is Facebook’s atom-based state management for React — atom({ key: "count", default: 0 }) creates a piece of state. useRecoilState(countAtom) reads and writes like useState. useRecoilValue(countAtom) reads without a setter. selector({ key, get }) computes derived values from atoms. atomFamily(config) creates a factory of atoms keyed by parameter — cartItemAtom("item-123") returns a unique atom per ID. selectorFamily does the same for derived values. Async selectors return Promises and integrate with React Suspense. atomEffects runs side effects on init, set, and reset — used for localStorage sync, URL params, and WebSocket subscriptions. useRecoilCallback creates a snapshot transaction to read multiple atoms outside React’s render cycle. waitForAll([a, b]) loads multiple atoms concurrently. Claude Code generates Recoil todo lists, shopping carts, multi-step forms, async data selectors, and persisted filter state.

CLAUDE.md for Recoil

## Recoil Stack
- Version: recoil >= 0.7.7 (stable)
- Provider: <RecoilRoot> wraps app root — no config needed
- Atom: const countAtom = atom({ key: "count", default: 0 })
- Selector: const doubleCount = selector({ key: "double", get: ({ get }) => get(countAtom) * 2 })
- Hooks: useRecoilState(atom) | useRecoilValue(selector) | useSetRecoilState(atom)
- Family: const itemAtom = atomFamily<Item, string>({ key: "item", default: id => defaultItem(id) })
- Async: selector with get returning Promise — wrap component in <Suspense>
- Effect: atomEffects for localStorage persistence — effects: [localStorageEffect("key")]

Core Atoms and Selectors

// store/cart.ts — Recoil cart state
import { atom, selector, atomFamily, selectorFamily, DefaultValue } from "recoil"

// Simple atoms
export const cartOpenAtom = atom<boolean>({
  key: "cart/open",
  default: false,
})

export const couponCodeAtom = atom<string | null>({
  key: "cart/couponCode",
  default: null,
})

// Cart items as a Set of IDs + per-item atoms
export const cartItemIdsAtom = atom<string[]>({
  key: "cart/itemIds",
  default: [],
})

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

export const cartItemAtom = atomFamily<CartItem | null, string>({
  key: "cart/item",
  default: null,
})

// Derived: all cart items without nulls
export const cartItemsSelector = selector<CartItem[]>({
  key: "cart/items",
  get: ({ get }) => {
    const ids = get(cartItemIdsAtom)
    return ids
      .map(id => get(cartItemAtom(id)))
      .filter((item): item is CartItem => item !== null)
  },
})

// Derived: subtotal
export const cartSubtotalSelector = selector<number>({
  key: "cart/subtotal",
  get: ({ get }) => {
    const items = get(cartItemsSelector)
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  },
})

// Derived: total with discount
export const cartTotalSelector = selector<number>({
  key: "cart/total",
  get: ({ get }) => {
    const subtotal = get(cartSubtotalSelector)
    const coupon = get(couponCodeAtom)
    // 10% discount for valid coupon
    const discount = coupon === "SAVE10" ? subtotal * 0.1 : 0
    return Math.max(0, subtotal - discount)
  },
})

// Derived: item count
export const cartItemCountSelector = selector<number>({
  key: "cart/itemCount",
  get: ({ get }) => {
    const items = get(cartItemsSelector)
    return items.reduce((sum, item) => sum + item.quantity, 0)
  },
})

Atom Effects (Persistence)

// store/effects.ts — atomEffect utilities
import { AtomEffect } from "recoil"

// Sync atom to localStorage
export function localStorageEffect<T>(key: string): AtomEffect<T> {
  return ({ setSelf, onSet, trigger }) => {
    // Load initial value from storage
    if (trigger === "get") {
      const saved = localStorage.getItem(key)
      if (saved != null) {
        try {
          setSelf(JSON.parse(saved) as T)
        } catch {
          localStorage.removeItem(key)
        }
      }
    }

    // Persist on every change
    onSet((newValue, _, isReset) => {
      if (isReset) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newValue))
      }
    })
  }
}

// Sync atom to URL search param
export function urlSearchParamEffect<T extends string | number>(
  paramName: string,
): AtomEffect<T> {
  return ({ setSelf, onSet, trigger }) => {
    if (trigger === "get" && typeof window !== "undefined") {
      const params = new URLSearchParams(window.location.search)
      const value = params.get(paramName)
      if (value != null) {
        setSelf(value as T)
      }
    }

    onSet((newValue, _, isReset) => {
      const url = new URL(window.location.href)
      if (isReset || newValue === "") {
        url.searchParams.delete(paramName)
      } else {
        url.searchParams.set(paramName, String(newValue))
      }
      window.history.replaceState(null, "", url.toString())
    })
  }
}

// Persisted filter state
export const searchQueryAtom = atom<string>({
  key: "search/query",
  default: "",
  effects: [urlSearchParamEffect("q")],
})

export const sortAtom = atom<"price_asc" | "price_desc" | "newest">({
  key: "search/sort",
  default: "newest",
  effects: [urlSearchParamEffect("sort"), localStorageEffect("search/sort")],
})

Async Selectors

// store/products.ts — async selectors with Suspense integration
import { atom, selector, selectorFamily } from "recoil"

// Filters — sync
export const categoryFilterAtom = atom<string | null>({
  key: "products/categoryFilter",
  default: null,
})

export const priceRangeAtom = atom<[number, number]>({
  key: "products/priceRange",
  default: [0, 10000],
})

// Async selector — triggers Suspense in component
export const filteredProductsSelector = selector({
  key: "products/filtered",
  get: async ({ get }) => {
    const category = get(categoryFilterAtom)
    const [minPrice, maxPrice] = get(priceRangeAtom)
    const query = get(searchQueryAtom)

    const params = new URLSearchParams()
    if (category) params.set("category", category)
    params.set("minPrice", String(minPrice))
    params.set("maxPrice", String(maxPrice))
    if (query) params.set("q", query)

    const res = await fetch(`/api/products?${params}`)
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return res.json() as Promise<Product[]>
  },
})

// Per-item async selector
export const productDetailSelector = selectorFamily<Product, string>({
  key: "products/detail",
  get: (id) => async () => {
    const res = await fetch(`/api/products/${id}`)
    if (!res.ok) throw new Error("Product not found")
    return res.json() as Promise<Product>
  },
})

type Product = {
  id: string
  name: string
  price: number
  category: string
  imageUrl: string
}

React Components

// components/cart/CartButton.tsx — reads Recoil cart state
"use client"
import { useRecoilValue, useSetRecoilState } from "recoil"
import { cartItemCountSelector, cartOpenAtom } from "@/store/cart"

export function CartButton() {
  const count = useRecoilValue(cartItemCountSelector)
  const setOpen = useSetRecoilState(cartOpenAtom)

  return (
    <button
      onClick={() => setOpen(prev => !prev)}
      className="relative p-2 rounded-xl hover:bg-muted"
      aria-label={`Cart — ${count} items`}
    >
      <span className="text-xl">🛒</span>
      {count > 0 && (
        <span className="absolute -top-1 -right-1 min-w-5 h-5 bg-primary text-primary-foreground text-xs rounded-full flex items-center justify-center px-1">
          {count > 99 ? "99+" : count}
        </span>
      )}
    </button>
  )
}

// components/products/ProductList.tsx — async selector with Suspense
"use client"
import { Suspense } from "react"
import { useRecoilValue, useSetRecoilState } from "recoil"
import { filteredProductsSelector, cartItemIdsAtom, cartItemAtom } from "@/store"
import { useRecoilCallback } from "recoil"

function ProductListInner() {
  const products = useRecoilValue(filteredProductsSelector)

  const addToCart = useRecoilCallback(({ set }) => (product: { id: string; name: string; price: number; imageUrl: string }) => {
    set(cartItemAtom(product.id), {
      id: product.id,
      productId: product.id,
      name: product.name,
      price: product.price,
      quantity: 1,
      imageUrl: product.imageUrl,
    })
    set(cartItemIdsAtom, prev =>
      prev.includes(product.id) ? prev : [...prev, product.id],
    )
  }, [])

  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
      {products.map(product => (
        <div key={product.id} className="rounded-xl border p-4 space-y-3">
          <img src={product.imageUrl} alt={product.name} className="w-full aspect-square object-cover rounded-lg" />
          <div>
            <h3 className="font-medium">{product.name}</h3>
            <p className="text-muted-foreground">${(product.price / 100).toFixed(2)}</p>
          </div>
          <button
            onClick={() => addToCart(product)}
            className="btn-primary w-full text-sm"
          >
            Add to Cart
          </button>
        </div>
      ))}
    </div>
  )
}

export function ProductList() {
  return (
    <Suspense fallback={
      <div className="grid grid-cols-3 gap-4">
        {Array.from({ length: 6 }).map((_, i) => (
          <div key={i} className="rounded-xl border p-4 space-y-3 animate-pulse">
            <div className="aspect-square bg-muted rounded-lg" />
            <div className="h-4 bg-muted rounded w-3/4" />
            <div className="h-4 bg-muted rounded w-1/2" />
          </div>
        ))}
      </div>
    }>
      <ProductListInner />
    </Suspense>
  )
}

For the Jotai alternative when a minimal, TypeScript-first atom state library without the verbose key string identifiers, provider-optional API, and First-class React Server Components support is preferred — Jotai is the spiritual successor to Recoil with a cleaner API, while Recoil has more community content and a more battle-tested async selector pattern, see the Jotai atoms guide. For the Zustand alternative when a simpler, non-atomic state store with a flat object API, no provider required, and better DevTools integration is preferred — Zustand is a single store rather than a graph of atoms, making it easier to reason about for most non-graph data structures, see the Zustand guide. The Claude Skills 360 bundle includes Recoil skill sets covering atoms, selectors, async loading, and persistence. Start with the free tier to try atom-based state 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