Valtio uses JavaScript Proxy to make plain objects reactive — const state = proxy({ count: 0 }) creates a reactive store. useSnapshot(state) returns an immutable snapshot for React rendering — components re-render only when accessed properties change. Mutate state directly: state.count++ or state.user = newUser. subscribe(state, () => console.log(snapshot(state))) watches changes outside React. derive({ doubled: get => get(state).count * 2 }) creates computed values. proxyWithComputed(initial, { computed }) colocates state and computed in one object. devtools(state) connects to Redux DevTools. proxySet() and proxyMap() provide reactive Set and Map. Valtio works standalone without React using snapshot(state) and subscribe. Claude Code generates Valtio stores, async action patterns, derived state, and component subscription patterns for minimal-boilerplate React state.
CLAUDE.md for Valtio
## Valtio Stack
- Version: valtio >= 1.13
- Store: const state = proxy({ count: 0, user: null as User | null })
- Read: const snap = useSnapshot(state) — reactive, snap.count triggers re-render
- Write: state.count++ / state.user = user — direct mutation outside React
- Computed: derive({ total: get => get(cartState).items.reduce(...) })
- Subscribe: subscribe(state, () => { /* snap = snapshot(state) */ }) — external
- DevTools: import { devtools } from "valtio/utils"; devtools(state, { name: "CartStore" })
- Collections: proxySet<string>() / proxyMap<string, Item>() — reactive Set/Map
- Snapshot: snapshot(state) — get plain object outside React
Store Setup
// stores/cart-store.ts — Valtio store with actions
import { proxy, subscribe, snapshot } from "valtio"
import { derive } from "valtio/utils"
import { devtools } from "valtio/utils"
export type CartItem = {
id: string
productId: string
name: string
priceCents: number
quantity: number
imageUrl: string
}
type CartState = {
items: CartItem[]
couponCode: string | null
discountCents: number
isLoading: boolean
error: string | null
}
export const cartState = proxy<CartState>({
items: [],
couponCode: null,
discountCents: 0,
isLoading: false,
error: null,
})
// Derived state — computed from cartState
export const cartDerived = derive({
subtotalCents: get => get(cartState).items.reduce(
(sum, item) => sum + item.priceCents * item.quantity,
0,
),
totalCents: get => {
const { items, discountCents } = get(cartState)
const subtotal = items.reduce((sum, item) => sum + item.priceCents * item.quantity, 0)
return Math.max(0, subtotal - discountCents)
},
itemCount: get => get(cartState).items.reduce((sum, item) => sum + item.quantity, 0),
isEmpty: get => get(cartState).items.length === 0,
})
// Actions — plain functions that mutate proxy
export function addItem(product: { id: string; name: string; priceCents: number; imageUrl: string }) {
const existing = cartState.items.find(i => i.productId === product.id)
if (existing) {
existing.quantity++
} else {
cartState.items.push({
id: crypto.randomUUID(),
productId: product.id,
name: product.name,
priceCents: product.priceCents,
quantity: 1,
imageUrl: product.imageUrl,
})
}
}
export function removeItem(itemId: string) {
const index = cartState.items.findIndex(i => i.id === itemId)
if (index >= 0) cartState.items.splice(index, 1)
}
export function updateQuantity(itemId: string, quantity: number) {
const item = cartState.items.find(i => i.id === itemId)
if (!item) return
if (quantity <= 0) {
removeItem(itemId)
} else {
item.quantity = quantity
}
}
export function clearCart() {
cartState.items = []
cartState.couponCode = null
cartState.discountCents = 0
}
// Async action — mutate state before/after async work
export async function applyCoupon(code: string) {
cartState.isLoading = true
cartState.error = null
try {
const res = await fetch(`/api/coupons/${code}`)
if (!res.ok) throw new Error("Invalid coupon code")
const { discountCents } = await res.json()
cartState.couponCode = code
cartState.discountCents = discountCents
} catch (err) {
cartState.error = err instanceof Error ? err.message : "Failed to apply coupon"
} finally {
cartState.isLoading = false
}
}
// Connect Redux DevTools in development
if (process.env.NODE_ENV === "development") {
devtools(cartState, { name: "CartStore", enabled: true })
}
// Persist cart to localStorage
subscribe(cartState, () => {
const snap = snapshot(cartState)
localStorage.setItem("cart", JSON.stringify({ items: snap.items, couponCode: snap.couponCode }))
})
// Hydrate from localStorage
function hydrateCart() {
try {
const stored = localStorage.getItem("cart")
if (stored) {
const { items, couponCode } = JSON.parse(stored)
cartState.items = items ?? []
cartState.couponCode = couponCode ?? null
}
} catch {
// Ignore parse errors
}
}
if (typeof window !== "undefined") hydrateCart()
React Components
// components/Cart.tsx — useSnapshot for reactive reads
"use client"
import { useSnapshot } from "valtio"
import { cartState, cartDerived, addItem, removeItem, updateQuantity, clearCart, applyCoupon } from "@/stores/cart-store"
import { useState } from "react"
export function CartSummary() {
// useSnapshot returns immutable snapshot — only re-renders on accessed fields
const cart = useSnapshot(cartState)
const derived = useSnapshot(cartDerived)
if (derived.isEmpty) {
return <p className="text-muted-foreground">Your cart is empty</p>
}
return (
<div className="space-y-3">
<p className="text-sm text-muted-foreground">{derived.itemCount} items</p>
<div className="space-y-2">
{cart.items.map(item => (
<CartItem key={item.id} item={item} />
))}
</div>
<div className="border-t pt-3 space-y-1">
<div className="flex justify-between text-sm">
<span>Subtotal</span>
<span>${(derived.subtotalCents / 100).toFixed(2)}</span>
</div>
{cart.discountCents > 0 && (
<div className="flex justify-between text-sm text-green-600">
<span>Discount ({cart.couponCode})</span>
<span>-${(cart.discountCents / 100).toFixed(2)}</span>
</div>
)}
<div className="flex justify-between font-semibold">
<span>Total</span>
<span>${(derived.totalCents / 100).toFixed(2)}</span>
</div>
</div>
<button onClick={clearCart} className="text-sm text-red-500 hover:underline">
Clear cart
</button>
</div>
)
}
// Fine-grained subscription — only re-renders when this item's data changes
function CartItem({ item }: { item: { id: string; name: string; priceCents: number; quantity: number } }) {
return (
<div className="flex items-center gap-3">
<div className="flex-1">
<p className="text-sm font-medium">{item.name}</p>
<p className="text-xs text-muted-foreground">${(item.priceCents / 100).toFixed(2)} each</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="size-6 rounded border text-sm"
>
−
</button>
<span className="w-8 text-center text-sm">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="size-6 rounded border text-sm"
>
+
</button>
</div>
<button onClick={() => removeItem(item.id)} className="text-xs text-red-500">
Remove
</button>
</div>
)
}
export function CouponInput() {
const cart = useSnapshot(cartState)
const [code, setCode] = useState("")
if (cart.couponCode) {
return (
<div className="flex items-center gap-2 text-sm text-green-600">
<span>Coupon applied: {cart.couponCode}</span>
</div>
)
}
return (
<div className="flex gap-2">
<input
value={code}
onChange={e => setCode(e.target.value)}
placeholder="Coupon code"
className="input flex-1"
/>
<button
onClick={() => applyCoupon(code)}
disabled={cart.isLoading || !code}
className="btn-secondary"
>
{cart.isLoading ? "Applying..." : "Apply"}
</button>
{cart.error && <p className="text-sm text-red-500">{cart.error}</p>}
</div>
)
}
proxySet and proxyMap
// stores/selection-store.ts — reactive Set and Map
import { proxy } from "valtio"
import { proxySet, proxyMap } from "valtio/utils"
type SelectionState = {
selectedIds: ReturnType<typeof proxySet<string>>
loadingIds: ReturnType<typeof proxySet<string>>
cache: ReturnType<typeof proxyMap<string, { data: unknown; timestamp: number }>>
}
export const selectionState = proxy<SelectionState>({
selectedIds: proxySet<string>(),
loadingIds: proxySet<string>(),
cache: proxyMap<string, { data: unknown; timestamp: number }>(),
})
export function toggleSelection(id: string) {
if (selectionState.selectedIds.has(id)) {
selectionState.selectedIds.delete(id)
} else {
selectionState.selectedIds.add(id)
}
}
export function selectAll(ids: string[]) {
ids.forEach(id => selectionState.selectedIds.add(id))
}
export function clearSelection() {
selectionState.selectedIds.clear()
}
// Component usage
function SelectableList({ items }: { items: { id: string; name: string }[] }) {
const sel = useSnapshot(selectionState)
return (
<ul>
{items.map(item => (
<li
key={item.id}
onClick={() => toggleSelection(item.id)}
className={sel.selectedIds.has(item.id) ? "bg-blue-50" : ""}
>
{item.name}
</li>
))}
</ul>
)
}
For the Zustand alternative when a slightly more structured store API with explicit set/get functions, middleware (immer, devtools, persist), and no Proxy magic is preferred — Zustand is more predictable for large teams and has first-class middleware support without runtime Proxy behavior, see the Zustand Advanced guide. For the Jotai alternative when atom-based granular state — where each piece of state is a separate atom that components subscribe to individually — is preferred over a single proxy object, Jotai avoids global stores entirely and integrates well with React Suspense and async atoms, see the Jotai guide. The Claude Skills 360 bundle includes Valtio skill sets covering proxy stores, derived state, and persistence patterns. Start with the free tier to try reactive state generation.