MCPcopy
hub / github.com/nanostores/nanostores

github.com/nanostores/nanostores @1.4.0 sqlite

repository ↗ · DeepWiki ↗ · release 1.4.0 ↗
82 symbols 330 edges 53 files 11 documented · 13%
README

Nano Stores

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 340 and 864 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>
  )
}

  Made at Evil Martians, product consulting for developer tools.


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.
  • SQL reactive store for SELECT from SQLite for browser or React Native.
  • 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}`)
})

Nested mutations can be listened to using a base key. For example, listening to user will trigger on user.name mutations:

// Mutating user.name triggers the listener
listenKeys($state, ['user'], (value, oldValue, changed) => {
  console.log(`user changed: ${changed}`)
})

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 { $posts } from './posts.js'

export const $newPosts = computed([$lastVisit, $posts], (lastVisit, posts) => {
  return posts.filter(post => post.publishedAt > lastVisit)
})

Effects

effect subscribes for multiple atoms at once.

effect runs its callback on the start, with initial values, as well as on any stores change. If callback returns cleanup function it will be performed before next callback run. Besides that, effect returns own cleanup function, which allows cancelling the whole effect.

const $enabled = atom(true)
const $interval = atom(1000)

const cancelPing = effect([$enabled, $interval], (enabled, interval) => {
  if (!enabled) return

  const intervalId = setInterval(() => {
    sendPing()
  }, interval)

  return () => {
    clearInterval(intervalId)
  }
})

Batching

batch groups multiple store writes into a single transaction. Listeners and effects fire at most once when the outermost batch returns, observing the final values. Batches can be nested; only the outermost flush triggers the notifications.

import { atom, batch } from 'nanostores'

const $firstName = atom('')
const $lastName = atom('')

batch(() => {
  $firstName.set('Ada')
  $lastName.set('Lovelace')
})
// effects depending on either atom run once, with both new values visible

Inside a batch, Map#setKey writes are coalesced too. The listener fires once and its changed argument is undefined (the batch touched multiple keys), so listenKeys subscribers fire once regardless of which key they watch. Outside a batch, each setKey still notifies with its own changed key as before.

Map Creator

If you have many similar stores (for instance, in advanced database ORM), you can define map creator (like a “class” in OOP).

const User = mapCreator((store, id) => {
  store.set({ id, isLoading: true })
  fetchUser(id).then(data => {
    store.set({ id, isLoading: false, data })
  })
})

let user1 = User('1')

Tasks

startTask() and task() can be used to mark all async operations during store initialization.

import { task } from 'nanostores'

onMount($post, () => {
  task(async () => {
    $post.set(await loadPost())
  })
})

You can wait for all ongoing tasks end in tests or SSR with await allTasks().

import { allTasks } from 'nanostores'

$post.listen(() => {}) // Move store to active mode to start data loading
await allTasks()

const html = ReactDOMServer.renderToString(<App />)

Store Events

Each store has a few events, which you listen:

  • onMount(store, cb): first listener was subscribed with debounce. We recommend to always use onMount instead of onStart + onStop, because it has a short delay to prevent flickering behavior.
  • onStart(store, cb): first listener was subscribed. Low-level method. It is better to use onMount for simple lazy stores.
  • onStop(store, cb): last listener was unsubscribed. Low-level method. It is better to use onMount for simple lazy stores.
  • onSet(store, cb): before applying any changes to the store.
  • onNotify(store, cb): before notifying store’s listeners about changes.

onSet and onNotify events has abort() function to prevent changes or notification.

import { onSet } from 'nanostores'

onSet($store, ({ newValue, abort }) => {
  if (!validate(newValue)) {
    abort()
  }
})

Event listeners can communicate with payload.shared object.

Integration

React & Preact

Use [@nanostores/react] or [@nanostores/preact] p

Extension points exported contracts — how you extend this code

MapCreator (Interface)
(no doc)
map-creator/index.d.ts
Task (Interface)
(no doc)
task/index.d.ts
Computed (Interface)
(no doc)
computed/index.d.ts
ReadableAtom (Interface)
(no doc)
atom/index.d.ts
MapStore (Interface)
(no doc)
map/index.d.ts
Effect (Interface)
(no doc)
effect/index.d.ts
WritableAtom (Interface)
(no doc)
atom/index.d.ts
PreinitializedMapStore (Interface)
(no doc)
map/index.d.ts

Core symbols most depended-on inside this repo

setKey
called by 121
map/index.d.ts
atom
called by 112
atom/index.js
set
called by 83
map/index.d.ts
listen
called by 82
map/index.d.ts
computed
called by 70
computed/index.js
get
called by 68
atom/index.d.ts
set
called by 63
atom/index.d.ts
listen
called by 48
atom/index.d.ts

Shape

Function 61
Method 12
Interface 9

Languages

TypeScript100%

Modules by API surface

atom/index.js10 symbols
atom/index.d.ts9 symbols
lifecycle/index.js8 symbols
map/index.d.ts7 symbols
deep-map/path.js6 symbols
task/index.js4 symbols
computed/index.js4 symbols
clean-stores/index.test.ts4 symbols
atom/index.test.ts3 symbols
test/benchmark.js2 symbols
task/index.test.ts2 symbols
map-creator/index.js2 symbols

Dependencies from manifests, versioned

@logux/oxc-configs0.4.0 · 1×
@sinonjs/fake-timers15.4.0 · 1×
@size-limit/preset-small-lib12.1.0 · 1×
@types/node26.0.1 · 1×
@types/sinonjs__fake-timers15.0.1 · 1×
@types/ws8.18.1 · 1×
actions-up1.14.3 · 1×
benchmark2.1.4 · 1×
better-node-test0.8.4 · 1×
check-dts1.0.0 · 1×
eslint-plugin-prefer-let4.2.2 · 1×
multiocular0.8.3 · 1×

For agents

$ claude mcp add nanostores \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact