Skip to main content
Technology & EngineeringState Management335 lines

Nanostores

Nanostores tiny framework-agnostic state manager — atoms, computed stores, lifecycle events, and integrations with React, Vue, Svelte, and vanilla JS

Quick Summary16 lines
You are an expert in using Nanostores for application state management across React, Vue, Svelte, and vanilla JavaScript.

## Key Points

- **Framework-agnostic** — one store, many frameworks (React, Vue, Svelte, Solid, vanilla JS)
- **Atom-based** — each store is an independent unit with its own subscribers
- **Tiny footprint** — core library under 1 KB gzipped
- **Lifecycle-aware** — `onMount` runs only when a store gains its first subscriber
- **Tree-shakeable** — unused stores and utilities are eliminated at build time
- **No Provider required** — stores are plain modules, imported directly
- **Use the `$` prefix convention** — naming stores with `$` (e.g., `$count`) makes it clear what is a reactive store vs. a plain variable, and aligns with Svelte's `$store` syntax.
- **Keep stores small and single-purpose** — one atom per concern. Combine with `computed` rather than building monolithic state objects.
- **Use `map()` for forms and key-value state** — `setKey()` is more efficient than replacing the entire object when only one field changes.
- **Creating circular computed dependencies** — computed store A depending on computed store B which depends on A causes infinite loops. Structure computed stores as a directed acyclic graph.
skilldb get state-management-skills/NanostoresFull skill: 335 lines
Paste into your CLAUDE.md or agent config

Nanostores — Tiny Framework-Agnostic State Management

You are an expert in using Nanostores for application state management across React, Vue, Svelte, and vanilla JavaScript.

Core Philosophy

Overview

Nanostores is a minimalist state manager designed for small, focused stores that work across any UI framework. Each store is an independent atom or computed value — there is no single root store. The library is intentionally tiny (under 1 KB) and relies on a simple subscribe/get API, with framework-specific integration packages providing hooks and bindings.

  • Framework-agnostic — one store, many frameworks (React, Vue, Svelte, Solid, vanilla JS)
  • Atom-based — each store is an independent unit with its own subscribers
  • Tiny footprint — core library under 1 KB gzipped
  • Lifecycle-awareonMount runs only when a store gains its first subscriber
  • Tree-shakeable — unused stores and utilities are eliminated at build time
  • No Provider required — stores are plain modules, imported directly

Setup & Configuration

# Core library
npm install nanostores

# Framework integration (pick one)
npm install @nanostores/react    # React / Preact
npm install @nanostores/vue      # Vue 3
# Svelte has built-in $ store syntax — no extra package needed
// stores/counter.ts
import { atom } from 'nanostores';

export const $count = atom(0);

export function increment() {
  $count.set($count.get() + 1);
}

export function decrement() {
  $count.set($count.get() - 1);
}
// React usage
import { useStore } from '@nanostores/react';
import { $count, increment, decrement } from '../stores/counter';

export function Counter() {
  const count = useStore($count);

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}
<!-- Svelte usage — native $ syntax works out of the box -->
<script>
  import { $count, increment, decrement } from '../stores/counter';
</script>

<button on:click={decrement}>-</button>
<span>{$count}</span>
<button on:click={increment}>+</button>

Core Patterns

Atoms

import { atom } from 'nanostores';

// Primitive atoms
export const $username = atom('');
export const $isLoggedIn = atom(false);
export const $selectedId = atom<string | null>(null);

// Object atom
interface User {
  id: string;
  name: string;
  email: string;
}

export const $currentUser = atom<User | null>(null);

// Reading and writing
$username.set('Alice');
const name = $username.get(); // 'Alice'

// Subscribing (vanilla JS)
const unsubscribe = $username.subscribe((value) => {
  console.log('Username changed to:', value);
});
unsubscribe();

Computed Stores

import { atom, computed } from 'nanostores';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

export const $cartItems = atom<CartItem[]>([]);
export const $taxRate = atom(0.08);

// Computed from one store
export const $cartCount = computed($cartItems, (items) =>
  items.reduce((sum, item) => sum + item.quantity, 0)
);

// Computed from multiple stores
export const $cartTotal = computed(
  [$cartItems, $taxRate],
  (items, taxRate) => {
    const subtotal = items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
    return subtotal * (1 + taxRate);
  }
);

Map Stores for Key-Value State

import { map } from 'nanostores';

interface FormState {
  name: string;
  email: string;
  message: string;
  submitting: boolean;
}

export const $contactForm = map<FormState>({
  name: '',
  email: '',
  message: '',
  submitting: false,
});

// Set individual keys without replacing the whole object
$contactForm.setKey('name', 'Alice');
$contactForm.setKey('submitting', true);

// Read the full value or subscribe
const formData = $contactForm.get();
// React usage with map
import { useStore } from '@nanostores/react';
import { $contactForm } from '../stores/contactForm';

export function ContactForm() {
  const form = useStore($contactForm);

  return (
    <form>
      <input
        value={form.name}
        onChange={(e) => $contactForm.setKey('name', e.target.value)}
      />
      <input
        value={form.email}
        onChange={(e) => $contactForm.setKey('email', e.target.value)}
      />
      <textarea
        value={form.message}
        onChange={(e) => $contactForm.setKey('message', e.target.value)}
      />
      <button disabled={form.submitting}>Send</button>
    </form>
  );
}

Lifecycle with onMount

import { atom, onMount } from 'nanostores';

interface Notification {
  id: string;
  text: string;
  read: boolean;
}

export const $notifications = atom<Notification[]>([]);

// onMount runs when the first subscriber appears
// and the returned cleanup runs when the last subscriber leaves
onMount($notifications, () => {
  const eventSource = new EventSource('/api/notifications/stream');

  eventSource.onmessage = (event) => {
    const notification = JSON.parse(event.data);
    $notifications.set([...$notifications.get(), notification]);
  };

  // Cleanup: close the connection when no one is listening
  return () => {
    eventSource.close();
  };
});

Async Actions

import { atom } from 'nanostores';

interface Post {
  id: number;
  title: string;
  body: string;
}

export const $posts = atom<Post[]>([]);
export const $postsLoading = atom(false);
export const $postsError = atom<string | null>(null);

export async function fetchPosts() {
  $postsLoading.set(true);
  $postsError.set(null);

  try {
    const res = await fetch('/api/posts');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data: Post[] = await res.json();
    $posts.set(data);
  } catch (err) {
    $postsError.set(err instanceof Error ? err.message : 'Unknown error');
  } finally {
    $postsLoading.set(false);
  }
}

export async function createPost(title: string, body: string) {
  const res = await fetch('/api/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title, body }),
  });
  const newPost: Post = await res.json();
  $posts.set([...$posts.get(), newPost]);
}

Sharing State Across Frameworks

// stores/theme.ts — shared between React and Vue micro-frontends
import { atom, computed } from 'nanostores';

export const $theme = atom<'light' | 'dark'>('light');

export const $bgColor = computed($theme, (theme) =>
  theme === 'dark' ? '#1a1a2e' : '#ffffff'
);

export function toggleTheme() {
  $theme.set($theme.get() === 'light' ? 'dark' : 'light');
}
// React micro-frontend
import { useStore } from '@nanostores/react';
import { $theme, toggleTheme } from '@shared/stores/theme';

export function ThemeButton() {
  const theme = useStore($theme);
  return <button onClick={toggleTheme}>Theme: {theme}</button>;
}
<!-- Vue micro-frontend -->
<script setup>
import { useStore } from '@nanostores/vue';
import { $theme, toggleTheme } from '@shared/stores/theme';

const theme = useStore($theme);
</script>

<template>
  <button @click="toggleTheme">Theme: {{ theme }}</button>
</template>

Best Practices

  • Use the $ prefix convention — naming stores with $ (e.g., $count) makes it clear what is a reactive store vs. a plain variable, and aligns with Svelte's $store syntax.
  • Keep stores small and single-purpose — one atom per concern. Combine with computed rather than building monolithic state objects.
  • Use map() for forms and key-value statesetKey() is more efficient than replacing the entire object when only one field changes.

Common Pitfalls

  • Calling .get() in React render without useStore.get() reads the current value but does not subscribe. The component will display the initial value and never update. Always use the framework-specific hook.
  • Creating circular computed dependencies — computed store A depending on computed store B which depends on A causes infinite loops. Structure computed stores as a directed acyclic graph.

Anti-Patterns

Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.

Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.

Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add state-management-skills

Get CLI access →