SkillAgentSearch skills...

Nanostores

A tiny (286 bytes) state manager for React/RN/Preact/Vue/Svelte with many atomic tree-shakable stores

Install / Use

/learn @nanostores/Nanostores
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Nano Stores

<img align="right" width="92" height="92" title="Nano Stores logo" src="https://nanostores.github.io/nanostores/logo.svg">

A tiny state manager for React, React Native, Preact, Vue, Svelte, Solid, Lit, Angular, and vanilla JS. It uses many atomic stores and direct manipulation.

  • Small. Between 271 and 802 bytes (minified and brotlied). Zero dependencies. It uses Size Limit to control size.
  • Fast. With small atomic and derived stores, you do not need to call the selector function for all components on every store change.
  • Tree Shakable. A chunk contains only stores used by components in the chunk.
  • Designed to move logic from components to stores.
  • Good TypeScript support.
// store/users.ts
import { atom } from 'nanostores'

export const $users = atom<User[]>([])

export function addUser(user: User) {
  $users.set([...$users.get(), user]);
}
// store/admins.ts
import { computed } from 'nanostores'
import { $users } from './users.ts'

export const $admins = computed($users, users => users.filter(i => i.isAdmin))
// components/admins.tsx
import { useStore } from '@nanostores/react'
import { $admins } from '../stores/admins.ts'

export const Admins = () => {
  const admins = useStore($admins)
  return (
    <ul>
      {admins.map(user => <UserItem user={user} />)}
    </ul>
  )
}

<img src="https://cdn.evilmartians.com/badges/logo-no-label.svg" alt="" width="22" height="16" />  Made at <b><a href="https://evilmartians.com/devtools?utm_source=nanostores&utm_campaign=devtools-button&utm_medium=github">Evil Martians</a></b>, product consulting for <b>developer tools</b>.


Table of Contents

Install

npm install nanostores

Smart Stores

  • Async computed store to fetch data or create chains of async operations.
  • Persistent store to save data to localStorage and synchronize changes between browser tabs.
  • Router store to parse URL and implements SPA navigation.
  • Media Query store sync value with media query.
  • Deep Map store to put big nested object/arrays and change keys by path.
  • I18n library based on stores to make application translatable.
  • Query store that helps you with smart remote data fetching.
  • Logux Client: stores with WebSocket sync and CRDT conflict resolution.
  • Immer plugin to enable immutable state updates using Immer.
  • qs manage the query string in the URL.

Devtools

  • Logger of lifecycle, changes in the browser console.
  • Vue Devtools plugin that detects stores and attaches them to devtools inspectors and timeline.

Guide

Atoms

Atom store can be used to store strings, numbers, arrays.

You can use it for objects too if you want to prohibit key changes and allow only replacing the whole object (like we do in router).

To create it call atom(initial) and pass initial value as a first argument.

import { atom } from 'nanostores'

export const $counter = atom(0)

In TypeScript, you can optionally pass value type as type parameter.

export type LoadingStateValue = 'empty' | 'loading' | 'loaded'

export const $loadingState = atom<LoadingStateValue>('empty')

Then you can use StoreValue<Store> helper to get store’s value type in TypeScript:

import type { StoreValue } from 'nanostores'

type Value = StoreValue<typeof $loadingState> //=> LoadingStateValue

store.get() will return store’s current value. store.set(nextValue) will change value.

$counter.set($counter.get() + 1)

store.subscribe(cb) and store.listen(cb) can be used to subscribe for the changes in vanilla JS. For React/Vue we have extra special helpers useStore to re-render the component on any store changes.

Listener callbacks will receive the updated value as a first argument and the previous value as a second argument.

const unbindListener = $counter.subscribe((value, oldValue) => {
  console.log(`counter value changed from ${oldValue} to ${value}`)
})

store.subscribe(cb) in contrast with store.listen(cb) also call listeners immediately during the subscription. Note that the initial call for store.subscribe(cb) will not have any previous value and oldValue will be undefined.

See also effect() if you want to subscribe to multiple stores.

Maps

Map store can be used to store objects with one level of depth and change keys in this object.

To create map store call map(initial) function with initial object.

import { map } from 'nanostores'

export const $profile = map({
  name: 'anonymous'
})

In TypeScript, you can pass type parameter with store’s type:

export interface ProfileValue {
  name: string,
  email?: string
}

export const $profile = map<ProfileValue>({
  name: 'anonymous'
})

store.set(object) or store.setKey(key, value) methods will change the store.

$profile.setKey('name', 'Kazimir Malevich')

Setting undefined will remove optional key:

$profile.setKey('email', undefined)

Store’s listeners will receive third argument with changed key.

$profile.listen((profile, oldProfile, changed) => {
  console.log(`${changed} new value ${profile[changed]}`)
})

You can also listen for specific keys of the store being changed, using listenKeys and subscribeKeys.

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

subscribeKeys(store, keys, cb) in contrast with listenKeys(store, keys, cb) also call listeners immediately during the subscription. Please note that when using subscribe for store changes, the initial evaluation of the callback has undefined old value and changed key.

Lazy Stores

A unique feature of Nano Stores is that every state has two modes:

  • Mount: when one or more listeners is mounted to the store.
  • Disabled: when store has no listeners.

Nano Stores was created to move logic from components to the store. Stores can listen for URL changes or establish network connections. Mount/disabled modes allow you to create lazy stores, which will use resources only if store is really used in the UI.

onMount sets callback for mount and disabled states.

import { onMount } from 'nanostores'

onMount($profile, () => {
  // Mount mode
  return () => {
    // Disabled mode
  }
})

For performance reasons, store will move to disabled mode with 1-second delay after last listener unsubscribing.

Call keepMount() to test store’s lazy initializer in tests and cleanStores to unmount them after test.

import { cleanStores, keepMount } from 'nanostores'
import { $profile } from './profile.js'

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

it('is anonymous from the beginning', () => {
  keepMount($profile)
  // Checks
})

Computed Stores

Computed store is based on other store’s value.

import { computed } from 'nanostores'
import { $users } from './users.js'

export const $admins = computed($users, users => {
  // This callback will be called on every `users` changes
  return users.filter(user => user.isAdmin)
})

Use [@nanostores/async] for async computed:

import { computedAsync } from '@nanostores/async'

const $org = computedAsync($orgSlug, slug => {
  return fetchJson(`/organizations/${slug}`)
})

// The callback receives the resolved org, not an AsyncValue wrapper.
const $profile = computedAsync([$org, $userId], (org, userId) => {
  return fetchJson(`/users/${org.id}/${userId}`)
})

By default, computed stores update each time any of their dependencies gets updated. If you are fine with waiting until the end of a tick, you can use batched. The only difference with computed is that it will wait until the end of a tick to update itself.

import { batched } from 'nanostores'

const $sortBy = atom('id')
const $categoryId = atom('')

export const $link = batched([$sortBy, $categoryId], (sortBy, categoryId) => {
  return `/api/entities?sortBy=${sortBy}&categoryId=${categoryId}`
})

// `batched` will update only once even you changed two stores
export function resetFilters () {
  $sortBy.set('date')
  $categoryIdFilter.set('1')
}

Both computed and batched can be calculated from multiple stores:

import { $lastVisit } from './lastVisit.js'
import {
View on GitHub
GitHub Stars7.2k
CategoryDevelopment
Updated3h ago
Forks139

Languages

TypeScript

Security Score

95/100

Audited on Mar 20, 2026

No findings