Claude Code for Nanostores: Tiny Framework-Agnostic State — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Nanostores: Tiny Framework-Agnostic State
Frontend

Claude Code for Nanostores: Tiny Framework-Agnostic State

Published: April 16, 2027
Read time: 6 min read
By: Claude Skills 360

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.

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