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 298 and 1013 bytes (minified and gzipped). 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.js'
export const $admins = computed($users, users => users.filter(i => i.isAdmin))
// components/admins.tsx
import { useStore } from '@nanostores/react'
import { $admins } from '../stores/admins.js'
export const Admins = () => {
const admins = useStore($admins)
return (
<ul>
{admins.map(user => <UserItem user={user} />)}
</ul>
)
}
Made in Evil Martians, product consulting for developer tools.
- Smart Stores
- Devtools
- Guide
- Integration
- Best Practices
- Known Issues
npm install nanostores
- Persistent store to save data
to
localStorage
and synchronize changes between browser tabs. - Router store to parse URL and implements SPA navigation.
- 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.
- Logger of lifecycles, changes and actions in the browser console.
- Vue Devtools plugin that detects stores and attaches them to devtools inspectors and timeline.
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')
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.
const unbindListener = $counter.subscribe(value => {
console.log('counter value:', value)
})
store.subscribe(cb)
in contrast with store.listen(cb)
also call listeners
immediately during the subscription.
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 second argument with changed key.
$profile.listen((profile, changed) => {
console.log(`${changed} new value ${profile[changed]}`)
})
Deep maps work the same as map
, but it supports arbitrary nesting of objects
and arrays that preserve the fine-grained reactivity.
import { deepMap, listenKeys } from 'nanostores'
export const $profile = deepMap({
hobbies: [
{
name: 'woodworking',
friends: [{ id: 123, name: 'Ron Swanson' }]
}
],
skills: [
[
'Carpentry',
'Sanding'
],
[
'Varnishing'
]
]
})
listenKeys($profile, ['hobbies[0].friends[0].name', 'skills[0][0]'])
// Won't fire subscription
$profile.setKey('hobbies[0].name', 'Scrapbooking')
$profile.setKey('skills[0][1]', 'Staining')
// But those will fire subscription
$profile.setKey('hobbies[0].friends[0].name', 'Leslie Knope')
$profile.setKey('skills[0][0]', 'Whittling')
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 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)
})
You can combine a value 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)
})
Action is a function that changes a store. It is a good place to move business logic like validation or network operations.
Wrapping functions with action()
can track who changed the store
in the logger.
import { action } from 'nanostores'
export const increase = action($counter, 'increase', (store, add) => {
if (validateMax(store.get() + add)) {
store.set(store.get() + add)
}
return store.get()
})
increase(1) //=> 1
increase(5) //=> 6
All running async actions are tracked by allTasks()
. It can simplify
tests with chains of actions.
import { allTasks } from 'nanostores'
renameAllPosts()
await allTasks()
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 />)
Async actions will be wrapped to task()
automatically.
rename($post1, 'New title')
rename($post2, 'New title')
await allTasks()
Each store has a few events, which you listen:
onMount(store, cb)
: first listener was subscribed with debounce. We recommend to always useonMount
instead ofonStart + 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 useonMount
for simple lazy stores.onStop(store, cb)
: last listener was unsubscribed. Low-level method. It is better to useonMount
for simple lazy stores.onSet(store, cb)
: before applying any changes to the store.onNotify(store, cb)
: before notifying store’s listeners about changes.onAction(store, cb)
: start, end and errors of asynchronous actions.
onSet
and onNotify
events has abort()
function to prevent changes
or notification.
import { onSet } from 'nanostores'
onSet($store, ({ newValue, abort }) => {
if (!validate(newValue)) {
abort()
}
})
onAction
event has two event handlers as properties inside:
onError
that catches uncaught errors during the execution of actions.onEnd
after events has been resolved or rejected.
import { onAction } from 'nanostores'
onAction($store, ({ id, actionName, onError, onEnd }) => {
console.log(`Action ${actionName} was started`)
onError(({ error }) => {
console.error(`Action ${actionName} was failed`, error)
})
onEnd(() => {
console.log(`Action ${actionName} was stopped`)
})
})
Event listeners can communicate with payload.shared
object.
Use @nanostores/react
or @nanostores/preact
package
and useStore()
hook to get store’s value and re-render component
on store’s changes.
import { useStore } from '@nanostores/react' // or '@nanostores/preact'
import { $profile } from '../stores/profile.js'
export const Header = ({ postId }) => {
const profile = useStore($profile)
return <header>Hi, {profile.name}</header>
}
Use @nanostores/vue
and useStore()
composable function
to get store’s value and re-render component on store’s changes.
<script setup>
import { useStore } from '@nanostores/vue'
import { $profile } from '../stores/profile.js'
const props = defineProps(['postId'])
const profile = useStore($profile)
</script>
<template>
<header>Hi, {{ profile.name }}</header>
</template>
Every store implements Svelte's store contract. Put $
before store variable
to get store’s value and subscribe for store’s changes.
<script>
import { profile } from '../stores/profile.js'
</script>
<header>Hi, {$profile.name}</header>
In other frameworks, Nano Stores promote code style to use $
prefixes
for store’s names. But in Svelte it has a special meaning, so we recommend
to not follow this code style here.
Use @nanostores/solid
and useStore()
composable function
to get store’s value and re-render component on store’s changes.
import { useStore } from '@nanostores/solid'
import { $profile } from '../stores/profile.js'
export function Header({ postId }) {
const profile = useStore($profile)
return <header>Hi, {profile().name}</header>
}
Use @nanostores/lit
and StoreController
reactive controller
to get store’s value and re-render component on store’s changes.
import { StoreController } from '@nanostores/lit'
import { $profile } from '../stores/profile.js'
@customElement('my-header')
class MyElement extends LitElement {
@property()
private profileController = new StoreController(this, $profile)
render() {
return html\`<header>Hi, ${profileController.value.name}</header>`
}
}
Use @nanostores/angular
and NanostoresService
with useStore()
method to get store’s value and subscribe for store’s changes.
// NgModule:
import { NANOSTORES, NanostoresService } from '@nanostores/angular';
@NgModule({
providers: [{ provide: NANOSTORES, useClass: NanostoresService }]
})
// Component:
import { Component } from '@angular/core'
import { NanostoresService } from '@nanostores/angular'
import { Observable, switchMap } from 'rxjs'
import { profile } from '../stores/profile'
import { IUser, User } from '../stores/user'
@Component({
selector: "app-root",
template: '<p *ngIf="(currentUser$ | async) as user">{{ user.name }}</p>'
})
export class AppComponent {
currentUser$: Observable<IUser> = this.nanostores.useStore(profile)
.pipe(switchMap(userId => this.nanostores.useStore(User(userId))))
constructor(private nanostores: NanostoresService) { }
}
Store#subscribe()
calls callback immediately and subscribes to store changes.
It passes store’s value to callback.
import { $profile } from '../stores/profile.js'
$profile.subscribe(profile => {
console.log(`Hi, ${profile.name}`)
})
Store#listen(cb)
in contrast calls only on next store change. It could be
useful for a multiple stores listeners.
function render () {
console.log(`${$post.get().title} for ${$profile.get().name}`)
}
$profile.listen(render)
$post.listen(render)
render()
See also listenKeys(store, keys, cb)
to listen for specific keys changes
in the map.
Nano Stores support SSR. Use standard strategies.
if (isServer) {
$settings.set(initialSettings)
$router.open(renderingPageURL)
}
You can wait for async operations (for instance, data loading
via isomorphic fetch()
) before rendering the page:
import { allTasks } from 'nanostores'
$post.listen(() => {}) // Move store to active mode to start data loading
await allTasks()
const html = ReactDOMServer.renderToString(<App />)
Adding an empty listener by keepMount(store)
keeps the store
in active mode during the test. cleanStores(store1, store2, …)
cleans
stores used in the test.
import { cleanStores, keepMount } from 'nanostores'
import { $profile } from './profile.js'
afterEach(() => {
cleanStores($profile)
})
it('is anonymous from the beginning', () => {
keepMount($profile)
expect($profile.get()).toEqual({ name: 'anonymous' })
})
You can use allTasks()
to wait all async operations in stores.
import { allTasks } from 'nanostores'
it('saves user', async () => {
saveUser()
await allTasks()
expect(analyticsEvents.get()).toEqual(['user:save'])
})
Stores are not only to keep values. You can use them to track time, to load data from server.
import { atom, onMount } from 'nanostores'
export const $currentTime = atom<number>(Date.now())
onMount($currentTime, () => {
$currentTime.set(Date.now())
const updating = setInterval(() => {
$currentTime.set(Date.now())
}, 1000)
return () => {
clearInterval(updating)
}
})
Use derived stores to create chains of reactive computations.
import { computed } from 'nanostores'
import { $currentTime } from './currentTime.js'
const appStarted = Date.now()
export const $userInApp = computed($currentTime, currentTime => {
return currentTime - appStarted
})
We recommend moving all logic, which is not highly related to UI, to the stores. Let your stores track URL routing, validation, sending data to a server.
With application logic in the stores, it is much easier to write and run tests. It is also easy to change your UI framework. For instance, add React Native version of the application.
Use a separated listener to react on new store’s value, not an action where you change this store.
const increase = action($counter, 'increase', store => {
store.set(store.get() + 1)
- printCounter(store.get())
}
+ $counter.listen(counter => {
+ printCounter(counter)
+ })
An action is not the only way for store to a get new value. For instance, persistent store could get the new value from another browser tab.
With this separation your UI will be ready to any source of store’s changes.
get()
returns current value and it is a good solution for tests.
But it is better to use useStore()
, $store
, or Store#subscribe()
in UI
to subscribe to store changes and always render the actual data.
- const { userId } = $profile.get()
+ const { userId } = useStore($profile)
Nano Stores use ESM-only package. You need to use ES modules in your application to import Nano Stores.
In Next.js ≥11.1 you can alternatively use the esmExternals
config option.
For old Next.js you need to use next-transpile-modules
to fix
lack of ESM support in Next.js.