一键导入
nanostores
nanostores state manager
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
nanostores state manager
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Write scripts and programs against gatOS — the KSA mod that exposes live Kitten Space Agency simulation state as a 9P filesystem at /sim (also over HTTP /v1 and MQTT). Use this when asked to read game/celestial/vehicle telemetry, control vehicles (throttle, ignite, staging, attitude, burns, RCS, lights, docking), use game/debug controls (teleport, impulse kick, refuel, time-warp, switch vessel), or write flight-computer / autopilot programs. Covers the full /sim catalog, the command model, KSA coordinate frames, and worked Bun/TypeScript + Rust examples.
Validate the gatOS mod against a new upstream Kitten Space Agency (KSA) game build — the break-check playbook run when the KSA game / its decompiled sources are bumped and you must decide whether gatOS needs changes. Covers exactly which gatOS surface couples to KSA and where, how to diff the CURRENT (new) vs PREVIOUS (old) decompiled sources, build-as-alarm, semantic-drift review, the reflection + render-internals coupling the compiler can't catch, and which docs to update in lockstep. Use when asked to "check gatOS against the new KSA build", "upgrade KSA", run the version-diff / break-check, or review a game update's impact. REQUIRES two KSA decompiled-source trees to be provided: CURRENT and PREVIOUS.
Author tutorial-style documentation for the gatOS Astro/Starlight docs site (under site/) — the progressive `guides/` series that teaches writing flight-computer / autopilot programs against the gatOS /sim filesystem and its HTTP /v1 mirror. Use this when asked to write, add, or revise a gatOS tutorial or guide, plan the tutorial curriculum, or turn a /sim feature into a lesson. Covers the house style, Starlight/MDX mechanics, the dual-transport (/sim file + HTTP) presentation convention, the reusable code-snippet library, and the beginner→advanced tutorial ladder. Pairs with the `gatos` skill (how the sim works) and docs/TUTORIAL_DATA_REFERENCE.md (the data tutorials are built from).
React Compiler automatically memoizes React components at build time, eliminating manual useMemo/useCallback/React.memo. Use when asked about React Compiler setup, memoization automation, "use memo"/"use no memo" directives, incremental adoption, compiler debugging, or build tool integration (Babel, Vite, Next.js, Expo, Metro, Rspack, Rsbuild).
React Rules of React — strict adherence required for React Compiler compatibility. Covers purity, idempotency, immutability, hook call rules, and component rendering rules. Use when writing any React component, custom hook, or reviewing React code.
Reference for Astro — the core web framework the gatOS docs site (under site/) is built on. Use this when authoring or editing pages in site/, working with .astro/.md/.mdx files, content collections, the astro.config.mjs, or component/JSX-in-Markdown mechanics. Covers Astro component anatomy, using components in MDX, content collections, frontmatter, imports, expressions, and the build/dev commands — enough to work the site without leaving the repo. Pair with the `starlight` skill (the docs theme + its components) and the `tutorials` skill (the gatOS house style).
| name | nanostores |
| description | nanostores state manager |
See react.md for react integration notes See persistent.md for persistent store integration notes
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.
npm 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}`)
})
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)
}
})
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 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) {}
}
Use @nanostores/alpine plugin and x-nano directive to bind stores
to Alpine.js components.
import Alpine from 'alpinejs'
import { NanoStores } from '@nanostores/alpine'
import { $profile } from '../stores/profile.js'
Alpine.plugin(NanoStores)
Alpine.magic('profile', () => $profile)
Alpine.start()
<div x-data x-nano:profile="$profile">
<header x-text="'Hi, ' + profile.name"></header>
</div>
nanotags is a thin Web Components wrapper powered by Nano Stores reactivity. It leans on the platform—Custom Elements, standard DOM, regular CSS—instead of reinventing them. The result is a typed, reactive component model with automatic cleanup in under 2.5 KB.
<x-counter count="0">
<span data-ref="display">0</span>
<button data-ref="increment">+1</button>
</x-counter>
import { define } from 'nanotags'
const Counter = define('x-counter')
.withProps(p => ({
count: p.number()
}))
.withRefs(r => ({
increment: r.one('button'),
display: r.one('span')
}))
.setup(ctx => {
ctx.on(ctx.refs.increment, 'click', () => {
ctx.props.$count.set(ctx.props.$count.get() + 1)
})
ctx.effect(ctx.props.$count, value => {
ctx.refs.display.textContent = String(value)
})
})
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 function where you change this store.
function increase() {
$counter.set($counter.get() + 1)
- printCounter(store.get())
}
+ $counter.listen(counter => {
+ printCounter(counter)
+ })
An action function 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() usage outside of testsget() 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.