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.
// 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.
get() usage outside of testsnpm install nanostores
localStorage and synchronize changes between browser tabs.SELECT from
SQLite for browser or React Native.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.
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.
A unique feature of Nano Stores is that every state has two modes:
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 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)
})
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)
}
})
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.
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')
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 />)
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.
Use [@nanostores/react] or [@nanostores/preact] p
$ claude mcp add nanostores \
-- python -m otcore.mcp_server <graph>