Nanostores is a tiny (~300B) framework-agnostic state management library — atom(initialValue) creates a primitive store. atom.get() reads it; atom.set(newValue) updates it. map({ key: value }) creates an object store; mapStore.setKey("key", value) updates a single key. computed([dep1, dep2], (v1, v2) => v1 + v2) creates derived state. onMount(store, () => { fetch...; return cleanup }) runs side effects when a store has subscribers. useStore(store) from @nanostores/react subscribes a React component. The same stores work in React, Vue, Svelte, and vanilla JS — ideal for shared state in micro-frontends or islands architecture. persistentAtom("key", initial, { encode/decode }) syncs to localStorage. Claude Code generates Nanostores atoms, maps, computed stores, React hooks, and persistence patterns for cross-framework shared state.
CLAUDE.md for Nanostores
## Nanostores Stack
- Version: nanostores >= 0.11, @nanostores/react >= 0.7
- Atom: const $count = atom(0); $count.get(); $count.set(1)
- Map: const $user = map({ name: "", email: "" }); $user.setKey("name", "Alice")
- Computed: const $total = computed([$cart], cart => cart.items.reduce(...))
- React: const count = useStore($count) — re-renders only when $count changes
- Lifecycle: onMount($store, () => { startPolling(); return () => stopPolling() })
- Persist: persistentAtom("theme", "light") from @nanostores/persistent
- Batch: batch(() => { $a.set(1); $b.set(2) }) — single re-render
Atom and Map Stores
// stores/ui-store.ts — UI state with atoms and maps
import { atom, map, computed } from "nanostores"
// Primitive atom — single value
export const $theme = atom<"light" | "dark">("light")
export const $sidebarOpen = atom(false)
export const $toastMessage = atom<string | null>(null)
// Object store — map for structured state
type UserPreferences = {
language: string
currency: string
notifications: boolean
compactMode: boolean
timezone: string
}
export const $preferences = map<UserPreferences>({
language: "en",
currency: "USD",
notifications: true,
compactMode: false,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
})
// Actions — plain functions that update stores
export function toggleTheme() {
$theme.set($theme.get() === "light" ? "dark" : "light")
}
export function toggleSidebar() {
$sidebarOpen.set(!$sidebarOpen.get())
}
export function showToast(message: string, duration = 3000) {
$toastMessage.set(message)
setTimeout(() => $toastMessage.set(null), duration)
}
export function updatePreference<K extends keyof UserPreferences>(
key: K,
value: UserPreferences[K],
) {
$preferences.setKey(key, value)
}
// stores/cart-store.ts — shopping cart with computed totals
import { atom, map, computed, onMount } from "nanostores"
type CartItem = {
id: string
productId: string
name: string
priceCents: number
quantity: number
imageUrl: string
}
type CartMap = { items: CartItem[]; coupon: string | null; discountCents: number }
export const $cart = map<CartMap>({
items: [],
coupon: null,
discountCents: 0,
})
// Computed stores — derived automatically
export const $cartItemCount = computed($cart, cart =>
cart.items.reduce((sum, item) => sum + item.quantity, 0),
)
export const $cartSubtotal = computed($cart, cart =>
cart.items.reduce((sum, item) => sum + item.priceCents * item.quantity, 0),
)
export const $cartTotal = computed(
[$cartSubtotal, $cart],
(subtotal, cart) => Math.max(0, subtotal - cart.discountCents),
)
export const $cartIsEmpty = computed($cart, cart => cart.items.length === 0)
// Cart actions
export function addToCart(product: Omit<CartItem, "id" | "quantity">) {
const { items } = $cart.get()
const existing = items.find(i => i.productId === product.productId)
if (existing) {
$cart.setKey(
"items",
items.map(i => i.productId === product.productId ? { ...i, quantity: i.quantity + 1 } : i),
)
} else {
$cart.setKey("items", [...items, { ...product, id: crypto.randomUUID(), quantity: 1 }])
}
}
export function removeFromCart(itemId: string) {
$cart.setKey("items", $cart.get().items.filter(i => i.id !== itemId))
}
export function updateQuantity(itemId: string, quantity: number) {
if (quantity <= 0) {
removeFromCart(itemId)
} else {
$cart.setKey(
"items",
$cart.get().items.map(i => i.id === itemId ? { ...i, quantity } : i),
)
}
}
// onMount — run when store has first subscriber, cleanup on last unsubscribe
onMount($cart, () => {
// Load cart from API
fetch("/api/cart")
.then(r => r.json())
.then(data => $cart.set(data))
.catch(() => {}) // Silently fail — use default empty state
return () => {
// Called when no components use this store anymore
// persist to server on unmount
}
})
React Components
// components/CartIcon.tsx — subscribe to multiple stores
"use client"
import { useStore } from "@nanostores/react"
import { $cartItemCount, $cartIsEmpty } from "@/stores/cart-store"
import { $sidebarOpen } from "@/stores/ui-store"
export function CartIcon() {
// Each useStore call creates a targeted subscription
const count = useStore($cartItemCount)
const isEmpty = useStore($cartIsEmpty)
return (
<button
onClick={() => $sidebarOpen.set(true)}
className="relative p-2 hover:bg-muted rounded-lg"
aria-label={`Cart${isEmpty ? "" : ` (${count} items)`}`}
>
<svg className="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
{!isEmpty && (
<span className="absolute -top-1 -right-1 size-5 bg-primary text-primary-foreground text-xs rounded-full flex items-center justify-center font-medium">
{count > 99 ? "99+" : count}
</span>
)}
</button>
)
}
// components/ThemeToggle.tsx
"use client"
import { useStore } from "@nanostores/react"
import { $theme, toggleTheme } from "@/stores/ui-store"
export function ThemeToggle() {
const theme = useStore($theme)
return (
<button onClick={toggleTheme} className="p-2 rounded-lg hover:bg-muted" aria-label="Toggle theme">
{theme === "light" ? "🌙" : "☀️"}
</button>
)
}
Persistent Store
// stores/settings-store.ts — persisted state with @nanostores/persistent
import { persistentAtom, persistentMap } from "@nanostores/persistent"
// Persists to localStorage automatically
export const $theme = persistentAtom<"light" | "dark">("theme", "light")
export const $recentSearches = persistentAtom<string[]>("recent-searches", [], {
encode: JSON.stringify,
decode: JSON.parse,
})
type ColumnVisibility = Record<string, boolean>
export const $columnVisibility = persistentMap<ColumnVisibility>("columns", {
name: true, email: true, created: true, plan: true, status: true,
})
// Actions
export function addRecentSearch(query: string) {
const recent = $recentSearches.get()
const updated = [query, ...recent.filter(q => q !== query)].slice(0, 10)
$recentSearches.set(updated)
}
export function clearRecentSearches() {
$recentSearches.set([])
}
For the Jotai alternative when atom-based state that lives closer to React’s component tree — with async atoms, atom families, and React Suspense integration — is preferred over Nanostores’ framework-agnostic approach, Jotai is the React-specific equivalent with better TypeScript inference for complex derived state, see the Jotai guide. For the Zustand alternative when a store with explicit set/get/subscribe functions, middleware (immer, persist, devtools), and better DevTools integration for larger applications is needed — Zustand is more appropriate when the state complexity outgrows Nanostores’ minimal primitives, see the Zustand Advanced guide. The Claude Skills 360 bundle includes Nanostores skill sets covering atoms, maps, computed state, and React integration. Start with the free tier to try lightweight state management generation.