Nanostores
Nanostores tiny framework-agnostic state manager — atoms, computed stores, lifecycle events, and integrations with React, Vue, Svelte, and vanilla JS
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 linesNanostores — 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-aware —
onMountruns 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$storesyntax. - Keep stores small and single-purpose — one atom per concern. Combine with
computedrather 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.
Common Pitfalls
- Calling
.get()in React render withoutuseStore—.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
Related Skills
Jotai Atomic State Management
Jotai atomic state for React — primitive/derived/async atoms, Provider-less mode, atomWithStorage, atomWithQuery, and React Suspense integration
Legend State
Legend-State high-performance observable state for React — fine-grained reactivity, persistence plugins, computed observables, and sync engine
Redux Toolkit
Redux Toolkit for scalable React state — createSlice, configureStore, RTK Query, createAsyncThunk, entity adapter, middleware, and TypeScript patterns
TanStack Query (React Query)
TanStack Query for server state — useQuery, useMutation, query invalidation, optimistic updates, infinite queries, prefetching, and SSR hydration
Valtio Proxy State Management
Valtio proxy-based state for React — mutable-style API with automatic tracking, snapshots, derived state, and nested object support
XState State Machines
XState for state machines and statecharts in React — actors, guards, actions, services, @xstate/react integration, and the visual editor