mrkeyoor.com_
Sat 08 Aug 17:43 UTC
npmWeb Frontendupdated 08 Aug 2026

nanostores

Nano Stores is a small, framework-neutral state manager built around many independent atoms, shallow maps, and computed stores. The core package has no runtime dependencies and can run in vanilla JavaScript; separate bindings connect the same stores to React, Preact, Vue, Svelte, Solid, Lit, Angular, and Alpine. Stores can start work only while observed, batch writes, expose lifecycle events, and track async tasks for tests or server rendering. It favors direct store functions over reducers, actions, or a central state tree.

Verdict

One of the best tiny choices for atomic state shared across frameworks, especially when lazy lifecycles matter. Skip it if you need old Node support, deep document updates, or batteries-included persistence and query tooling.

API stability4/5The core vocabulary is compact and internally consistent: atom, map, computed, subscribe, listen, onMount, and explicit setters cover most use. Version 1.4 adds batching without replacing those fundamentals. The main compatibility boundary is packaging rather than store semantics, since current releases are ESM-only and require modern Node, while integrations and smart stores live in separately versioned packages with their own APIs.
Docs5/5The README is a full working guide rather than a marketing stub. It explains immediate versus deferred subscriptions, shallow maps, lazy mount timing, computed and batched updates, effects, async task tracking, SSR, tests, and concrete adapters for nine UI environments. It also points out preferred practices and separate packages for missing capabilities, though production SSR isolation and migration guidance require more judgment than the examples provide.
Maintenance5/5Version 1.4.2 was published and the repository pushed on July 29, 2026. The repository is not archived, has 7,540 stars, and GitHub reports only 21 open issues and pull requests. Size Limit is used to guard the core's footprint, TypeScript declarations ship with the package, and recent documentation includes new batching behavior and a wide integration matrix, all signs of active, disciplined maintenance.
Ecosystem4/5The npm download API reports 7,216,793 downloads in the latest week. Official or linked adapters cover React, React Native, Preact, Vue, Svelte, Solid, Lit, Angular, Alpine, Web Components, and vanilla JavaScript, while companion stores cover persistence, routing, async computation, media queries, queries, and more. That breadth is impressive, but the ecosystem is distributed across many small packages and lacks Redux's depth of middleware and debugging conventions.

Use it if

  • You want shared state that can survive a gradual migration between UI frameworks or power both framework and vanilla code
  • Your state splits naturally into small atoms and derived values rather than one deeply nested application tree
  • Bundle cost matters and you want a dependency-free, tree-shakable core with optional framework bindings
  • Stores need lazy setup and cleanup for timers, subscriptions, media queries, or network connections
Skip it if

Setup reality

The core install is genuinely small: npm install nanostores, no runtime dependencies, included TypeScript declarations, and an ESM-only package. The first compatibility check is Node: 1.4.2 accepts Node 20.x or 22 and newer, not Node 18 or 21. UI projects need a second package such as @nanostores/react, @nanostores/vue, or @nanostores/preact; each framework has its own useStore shape, and Svelte can use the store contract directly while its runes API uses another adapter. Remote fetching, localStorage persistence, routing, media queries, query caching, deep maps, and dev logging are separate packages, so the tiny core number does not describe a feature-rich app's final dependency set. Store modules are mutable singletons by default. In a browser that is convenient; in SSR it means you must initialize per-request values carefully and avoid concurrent requests sharing user state. allTasks only waits for work registered through task, and lazy onMount initialization starts only after a listener mounts the store. Cleanup is delayed by one second after the last subscriber, which prevents mount flicker but can surprise fake-timer tests. subscribe fires immediately with oldValue undefined, while listen waits for a change. Map setKey only handles shallow keys; setting an optional key to undefined removes it. React and other integrations rerender through their adapter, so importing a store and calling get inside render will not subscribe. Testing lazy stores usually requires keepMount and cleanStores. None of this is hard, but the library rewards teams that keep actions next to stores and make lifecycle ownership explicit.

Patterns

Create and update a typed atomcreate-atom

import { atom } from 'nanostores'

export const $count = atom<number>(0)

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

get reads a snapshot but does not subscribe. UI code should use its framework adapter so future changes rerender.

Subscribe and clean up in vanilla JavaScriptsubscribe-in-vanilla-js

const unsubscribe = $count.subscribe((value, oldValue) => {
  counter.textContent = String(value)
})

// when the owning view is removed
unsubscribe()

subscribe calls the callback immediately, with oldValue undefined on that first call. listen only runs after a later change.

Update one key in a map storestore-shallow-object

import { map } from 'nanostores'

type Profile = { name: string; email?: string }
export const $profile = map<Profile>({ name: 'Anonymous' })

$profile.setKey('name', 'Ada')
$profile.setKey('email', undefined)

Maps are designed for one level of keys. Setting an optional key to undefined removes it; use a deep-map package for path-based nested updates.

React only to selected map keyslisten-to-map-key

import { listenKeys } from 'nanostores'

const unbind = listenKeys($profile, ['name'], (value, oldValue, changed) => {
  console.log(changed, oldValue.name, value.name)
})

listenKeys waits for changes. subscribeKeys also runs immediately, where oldValue and changed are undefined.

Derive a value from multiple storesderive-computed-store

import { computed } from 'nanostores'

export const $visibleTodos = computed(
  [$todos, $filter],
  (todos, filter) => todos.filter(todo =>
    filter === 'all' ? true : todo.done === (filter === 'done')
  )
)

computed recalculates for every dependency update. Use batched when several related writes should produce only one derived update at the end of the tick.

Notify once after related writesbatch-store-writes

import { batch } from 'nanostores'

batch(() => {
  $firstName.set('Ada')
  $lastName.set('Lovelace')
})

Listeners and effects run at most once after the outermost batch. For batched map key changes, the listener's changed argument is undefined.

Start work only while a store is observedmanage-lazy-lifecycle

import { atom, onMount } from 'nanostores'

export const $online = atom(false)

onMount($online, () => {
  const update = () => $online.set(navigator.onLine)
  update()
  window.addEventListener('online', update)
  window.addEventListener('offline', update)
  return () => {
    window.removeEventListener('online', update)
    window.removeEventListener('offline', update)
  }
})

Cleanup runs after a one-second delay once the final listener unsubscribes. Keep browser globals inside onMount so importing the module during SSR stays safe.

Run and clean up a multi-store effectrun-reactive-effect

import { effect } from 'nanostores'

const cancel = effect([$enabled, $interval], (enabled, delay) => {
  if (!enabled) return
  const id = setInterval(sendHeartbeat, delay)
  return () => clearInterval(id)
})

// later
cancel()

The callback runs immediately and on changes. Its cleanup runs before the next execution, while cancel removes the effect completely.

Render a store in Reactuse-store-in-react

import { useStore } from '@nanostores/react'
import { $profile } from './stores/profile.js'

export function Header() {
  const profile = useStore($profile)
  return <header>Hello, {profile.name}</header>
}

Install @nanostores/react separately. Calling $profile.get() in the component would read once without subscribing React to updates.

Register async initialization for SSR and teststrack-async-task

import { atom, onMount, task } from 'nanostores'

export const $post = atom(null)
onMount($post, () => {
  task(async () => {
    $post.set(await fetch('/api/post/1').then(r => r.json()))
  })
})

Only work wrapped in task is visible to allTasks. A listener must mount the lazy store before its task begins.

Wait for store tasks before server renderingprepare-server-render

import { allTasks } from 'nanostores'

const unbind = $post.listen(() => {})
await allTasks()
const html = renderToString(<App />)
unbind()

Module-level stores are shared by the server process. Create an isolation strategy for user-specific values before handling concurrent requests.

Mount and clean a lazy store in teststest-lazy-store

import { cleanStores, keepMount } from 'nanostores'

afterEach(() => {
  cleanStores($profile)
})

it('starts anonymous', () => {
  keepMount($profile)
  expect($profile.get()).toEqual({ name: 'Anonymous' })
})

keepMount activates onMount logic without a UI subscriber. cleanStores forces cleanup and prevents store state or timers from crossing tests.

Alternatives

PackageRegistryPick it when
zustandnpmA React-first team wants a hook-oriented store, selectors, middleware, and a larger integration ecosystem
jotainpmReact is the only target and atom composition should follow React's component and Suspense model
valtionpmMutable proxy objects feel more natural than explicit atom set calls in a React application
reduxnpmA large team values centralized actions, middleware, replayable state changes, and established debugging conventions