Nano Stores(npmパッケージ名は nanostores)は、状態を小さなストアに分けて持ち、React・Vue・Svelte・Solid・Preact・Lit・Angular・素のJavaScriptから同じストアを読み書きできる状態管理ライブラリです。作者はAndrey Sitnik氏です。Astroの公式ドキュメントが、アイランド間で状態を共有する方法として推奨しています。
この記事は2026年10月時点の最新版 nanostores 1.5.5・@nanostores/react 2.0.1・@nanostores/async 0.1.1・@nanostores/persistent 1.3.5 を基準にしています。掲載したコードの挙動とバンドルサイズは Node.js v26.5.0・React 19.3.1 で実行して確かめた結果で、Astro・Vueの例は公式ドキュメントの手順に基づいています。
まとめ:Nano Storesの要点と選ぶ条件
- 最新版は nanostores 1.5.5(2026-10-06公開)。ESM専用で、1.0で
effect()、1.4でbatch()、1.5で比較関数eqが加わりました。 - 基本は
atom(単一値)・map(1階層のオブジェクト)・computed(派生値)の3つです。複数ストアへの反応はeffect()、書き込みの検証はonSet()、ID別のストアはmapCreator()で書けます。 - Reactでは
@nanostores/reactのuseStore()で値を読みます。非同期ストアをuseLoadingStore()で読むとSuspenseが使えますが、renderToStringは読み込み完了を待たないため、描画中にサスペンドした場合はフォールバックを出力します。 - @nanostores/async は0.1.1で状態のフィールド名が
stateからstatusに変わりました。READMEの例は旧名のままです。 - Astroでは、別々のアイランドや別のフレームワークのコンポーネントが同じストアを共有できます。
.astroのフロントマターから書き込んでも、クライアント側には届きません。 persistentAtomは文字列で保存します。数値をencode/decodeなしで入れると、再読み込み後や、disabled状態からの読み直しで保存値を復元したときに文字列で戻ることがあります。- React専用のアプリなら、バンドルサイズはZustandのほうが小さく出ました(実測349バイト対903バイト)。Nano Storesが向くのは、複数のフレームワークをまたぐ場面や、ロジックをコンポーネントの外に出したい場面です。
Nano Storesの特徴と1.x系のパッケージ構成
公式READMEは特徴を「Small」「Fast」「Tree Shakable」と書いています。サイズは「Between 351 and 844 bytes (minified and brotlied)」で、依存パッケージはゼロです。0.11.4と1.0.1のREADMEは「Between 265 and 803 bytes」だったので、1.x系の機能追加で少し大きくなっています。
設計の考え方は、大きなストアを1つ持つのではなく、値ごとに小さなストアを作ることです。コンポーネントは自分が使うストアだけを購読するので、関係のないストアが変わっても再描画されません。READMEはさらに、URLの監視・通信・バリデーションといったUIと関係の薄いロジックをストア側に置くよう勧めています。
主要パッケージの最新版(2026年10月31日時点)
本体と連携パッケージは版番号がそろっていません。npmのregistry APIで各パッケージの dist-tags.latest と公開日時(UTC)を2026年10月31日に確認した結果は次のとおりです。
| パッケージ | 最新版 | 公開日 | 用途 |
|---|---|---|---|
| nanostores | 1.5.5 | 2026-10-06 | 本体 |
| @nanostores/react | 2.0.1 | 2026-09-02 | React連携 |
| @nanostores/vue | 1.1.0 | 2026-03-07 | Vue連携 |
| @nanostores/preact | 1.1.1 | 2026-10-05 | Preact連携 |
| @nanostores/solid | 1.1.1 | 2025-08-22 | Solid連携 |
| @nanostores/svelte-runes | 1.0.1 | 2026-07-25 | Svelte 5のrunes連携 |
| @nanostores/persistent | 1.3.5 | 2026-07-20 | localStorage保存 |
| @nanostores/async | 0.1.1 | 2026-10-20 | 非同期の派生ストア |
| @nanostores/router | 2.0.1 | 2026-10-26 | URLルーティング |
1.5.5の変更は、LLMがコードを書くときの手掛かりになるコメントの追加だけで、APIは1.5.4と同じです。@nanostores/react 2.0.0と@nanostores/router 2.0.0はNode.js 20のサポートを外しました。本体のpackage.jsonは引き続きNode.js 20を動作環境に含めています。
1.0以降に追加・非推奨になったAPI
0.x系の解説記事を読むと、今の版とAPIが食い違う箇所があります。公式のCHANGELOGから主な変更を拾いました。
| 版 | 変更 |
|---|---|
| 1.0.0 | effect()・getKey() 追加、Node.js 18のサポート終了 |
| 1.1.0 | deepMap() を非推奨にし @nanostores/deepmap へ分離 |
| 1.2.0 | 非同期の computed を非推奨にし @nanostores/async へ |
| 1.4.0 | 複数ストアの書き込みをまとめる batch() 追加 |
| 1.5.0 | 値の比較関数 eq・eqKey 追加 |
旧記事で見かける derive という関数は、1.5.5のエクスポートにありません。派生値は computed か batched で作ります。
atom・map・computedの基本と挙動
インストールは npm install nanostores だけです。パッケージは "type": "module" のESM専用で、require() で読み込む古いCommonJSの構成には合いません。ストアの変数名は $ で始めるのがREADMEの慣例です。
atomの作成と更新・購読の方法
atom(初期値) で単一の値を持つストアを作ります。get() で読み、set() で書き、subscribe() か listen() で変更を受け取ります。subscribe() は登録した瞬間に現在値で1回呼ばれ、listen() は次の変更から呼ばれます。
import { atom } from 'nanostores'
export const $counter = atom(0)
export function increase() {
$counter.set($counter.get() + 1)
}
const unbind = $counter.subscribe((value, oldValue) => {
console.log(`${oldValue} -> ${value}`)
})
unbind() // 購読をやめる
set() は新旧の値を Object.is() で比べ、同じなら何もしません。配列に push() してから同じ配列を set() しても、リスナーは呼ばれません。実行すると呼び出し回数は0回で、スプレッド構文で新しい配列を作ると1回になりました。
const $list = atom([1])
$list.listen(() => console.log('changed'))
const arr = $list.get()
arr.push(2)
$list.set(arr) // 呼ばれない(同じ参照)
$list.set([...$list.get(), 3]) // 呼ばれる
mapストアのsetKeyとlistenKeys
map() は1階層のオブジェクトを持ち、setKey() でキー単位に更新できます。特定のキーの変化だけを拾いたいときは listenKeys() を使います。
import { map, listenKeys } from 'nanostores'
export const $profile = map({ name: 'anonymous', email: '' })
listenKeys($profile, ['email'], value => {
console.log('email:', value.email)
})
$profile.setKey('name', 'Kazimir') // 呼ばれない
$profile.setKey('email', '[email protected]') // 呼ばれる
実行すると、name を変えたときの呼び出しは0回、email を変えたときに1回でした。setKey(key, undefined) を渡すと任意キーを削除できます。
computedとbatched・batch()の再計算回数
computed は依存するストアが変わるたびに計算し直します。batched は同じティックの変更をまとめ、ティックの終わりに1回だけ計算します。
import { atom, computed, batched, batch } from 'nanostores'
const $a = atom(1)
const $b = atom(1)
const $sum = computed([$a, $b], (a, b) => a + b)
const $sumBatched = batched([$a, $b], (a, b) => a + b)
$sum.listen(() => {}) // 購読して mount 状態にする
$sumBatched.listen(() => {})
$a.set(2)
$b.set(3)
// 購読後の更新で computed: 2回計算 / batched: ティック終了後に1回
await new Promise(resolve => setTimeout(resolve, 0))
batch(() => {
$a.set(10)
$b.set(20)
})
// $sum のリスナーは1回だけ呼ばれ、値は30
計算回数は購読を始めた後の更新分だけを数えています。派生ストアは購読も get() もされていなければ計算されません。batched が1ティック遅れて計算するのに対し、1.4で入った batch() は書き込む側でまとめ、外側の関数を抜けた時点で同期的に通知します。フィルタ条件のように複数のatomを一度に書き換える操作は batch() で包むと、途中の組み合わせで描画が走りません。派生値が配列やオブジェクトを毎回作り直す場合は、1.5の eq に深い比較関数を渡すと、中身が同じときの通知を止められます。
onMountによる遅延初期化と1秒の猶予
ストアは、購読者がいる「mount」状態と、いない「disabled」状態を持ちます。onMount() に渡した処理は最初の購読者が付いたときに動き、戻り値の関数は購読者がいなくなったときに動きます。画面で使われていないストアは通信やタイマーを始めない、という作り方ができます。
import { atom, onMount } from 'nanostores'
export const $now = atom(Date.now())
onMount($now, () => {
const id = setInterval(() => $now.set(Date.now()), 1000)
return () => clearInterval(id)
})
最後の購読者が外れてから disabled になるまで1秒の遅延があり、購読を外して計ると後始末の関数が呼ばれたのは1,001ミリ秒後でした。ページ遷移で一瞬だけ購読者が0になっても、接続を張り直さずに済むための猶予です。
effect・onSet・mapCreatorによるストア側のロジック管理
READMEのBest Practicesは、通信やバリデーションをコンポーネントからストアへ移すよう勧めています。そのための関数が effect()・onSet()・mapCreator() で、テストの書き方もREADMEに載っています。
effect()による複数ストアの購読と後始末
effect() は複数のストアをまとめて購読し、登録した時点と、どれかの値が変わるたびにコールバックを呼びます。コールバックが返した関数は次の実行の前に呼ばれ、effect() 自身の戻り値で全体を止められます。
import { atom, effect } from 'nanostores'
const $enabled = atom(true)
const $interval = atom(1000)
const cancelPing = effect([$enabled, $interval], (enabled, interval) => {
if (!enabled) return
const id = setInterval(sendPing, interval)
return () => clearInterval(id)
})
呼び出し順を記録すると、登録時に「1000で実行」、$interval を500にすると「後始末→500で実行」、$enabled をfalseにすると「後始末→実行」の順でした。enabled がfalseのときもコールバック自体は呼ばれるので、先頭の早期returnは省けません。この例ではfalseの時点で後始末関数を返していないため、続けて cancelPing() を呼んでも後始末は走らず、購読だけが解除されてその後の変更では呼ばれなくなります。
onSetによる書き込み前の検証と中止
onSet() は値が書き込まれる直前に呼ばれ、abort() で書き込みを取り消せます。どのコンポーネントから set() されても同じ検証を通せるのが、フォーム側で検証する書き方との違いです。
import { atom, onSet } from 'nanostores'
export const $age = atom(20)
onSet($age, ({ newValue, abort }) => {
if (newValue < 0) abort()
})
$age.set(-1) // 取り消される(get() は 20 のまま)
$age.set(30) // 反映される
通知の直前に呼ばれる onNotify() にも abort() があり、こちらは値を書き換えたうえでリスナーへの通知だけを止めます。
mapCreatorによるID別ストアの生成と遅延初期化
ユーザーや商品のようにIDごとに同じ形のストアが要るときは、mapCreator() で「ストアの作り方」を定義します。JotaiのatomFamilyに近い使い方です。
import { mapCreator } from 'nanostores'
export const User = mapCreator((store, id) => {
store.set({ id, isLoading: true })
fetchUser(id).then(data => store.set({ id, isLoading: false, data }))
})
const user1 = User('1')
User('1') === user1 // true(同じIDは同じストア)
実行すると、User('1') を呼んだ時点では初期化関数は動かず、get() か購読をした時点で初めて動きました。一覧に100件のIDを並べても、画面で読まれたストアの分しか fetchUser() は走りません。
テストでのkeepMount・cleanStores・allTasks
READMEのTestsは、keepMount() でストアをmount状態に保ち、afterEach で cleanStores() を呼んで後始末する書き方を示しています。モジュールで共有したストアは、cleanStores() で購読やライフサイクルを後始末しても、値が初期値に戻るとは限りません。テスト間の値を分離するには、各テストでストアを作り直すか、初期値を明示的に設定します。task() で包んだ非同期処理は await allTasks() でまとめて待てます。
import { cleanStores, keepMount, allTasks } from 'nanostores'
import { $profile, saveUser } from './profile.js'
afterEach(() => {
cleanStores($profile)
})
it('saves user', async () => {
keepMount($profile)
saveUser()
await allTasks()
expect($profile.get().saved).toBe(true)
})
Reactでの使い方(@nanostores/react 2.0.1)
Reactから使うには npm install nanostores @nanostores/react を実行し、useStore() でストアを読みます。ストアはコンポーネントの外で定義するので、ContextのProviderで包む必要はありません。
import { useStore } from '@nanostores/react'
import { $profile } from '../stores/profile'
export function Header() {
const profile = useStore($profile, { keys: ['name'] })
return <header>Hi, {profile.name}</header>
}
keys オプションを付けると、mapストアのうち指定したキーが変わったときだけ再描画します。上の例では email が変わっても Header は描画し直されません。2.0.0でNode.js 20のサポートが外れ、package.jsonの動作環境は Node.js 22・24・26以降です。
SSRでの初期値とssrオプション
サーバーでストアを書き換えてからHTMLを生成すると、クライアントの初回描画と値が食い違い、ハイドレーションエラーの原因になります。@nanostores/react 1.1.0で入った ssr: 'initial' を指定すると、サーバーでは初期値で描画し、クライアントで最新値に切り替えます。
const $n = atom(0)
$n.set(7) // サーバー側で書き換えた
useStore($n) // renderToString の結果: 7
useStore($n, { ssr: 'initial' }) // renderToString の結果: 0
react-dom/server の renderToString で描画すると、指定なしは7、ssr: 'initial' は0を出力しました。
@nanostores/asyncで非同期データを扱う方法
本体の computed に非同期関数を渡す書き方は1.2.0で非推奨になり、@nanostores/async の computedAsync に移りました。依存するストアが変わると取得し直し、値を「読み込み中・取得済み・失敗」の状態つきで持ちます。
computedAsyncの状態遷移とstatusへの改名
import { atom } from 'nanostores'
import { computedAsync } from '@nanostores/async'
export const $userId = atom('u1')
export const $user = computedAsync($userId, id => fetchUser(id))
$user.subscribe(user => {
if (user.status === 'loading') showSpinner()
if (user.status === 'ready') showProfile(user.value)
if (user.status === 'failed') showError(user.error)
})
IDを u1→u2→取得に失敗するID の順に切り替えて値を記録すると、次の順に遷移しました。
| きっかけ | status | changing | value・error |
|---|---|---|---|
| 購読開始 | loading | なし | なし |
| u1の取得完了 | ready | false | u1のデータ |
| IDをu2に変更 | ready | true | u1のデータ |
| u2の取得完了 | ready | false | u2のデータ |
| 失敗するIDに変更 | ready | true | u2のデータ |
| 取得で例外 | failed | false | Error |
取り直しの間は loading に戻らず、前の値を持ったまま changing: true になります。画面全体をスピナーに替えず、前のデータの上に小さな更新表示を出す作りが素直に書けます。
0.1.1(2026-10-20)で、この状態のフィールド名が state から status に変わりました。型定義では state に @deprecated use `status` が付き、実行時は両方に同じ値が入ります。READMEのコード例はまだ user.state で書かれているので、写すときは status に置き換えます。
ReactのuseLoadingStoreとSuspense
@nanostores/react 2.0.0で入った useLoadingStore() は、読み込み中なら最寄りの <Suspense> のフォールバックを表示し、失敗ならエラーバウンダリへ例外を投げ、取得済みなら値そのものを返します。
import { Suspense } from 'react'
import { useLoadingStore } from '@nanostores/react'
import { $user } from '../stores/user'
function Profile() {
const user = useLoadingStore($user) // ready のときは value が返る
return <p>{user.name}</p>
}
export function Page() {
return (
<Suspense fallback={<p>読み込み中</p>}>
<Profile />
</Suspense>
)
}
このコンポーネントをサーバーで描画すると、API次第で結果が分かれました。ストアが読み込み中のまま描画すると、renderToString はSuspenseを待てないため「読み込み中」だけを出力し、クライアント描画に切り替える旨のメッセージをHTMLに埋め込みます。React 19の prerender(react-dom/static)は取得の完了を待ち、ユーザー名の入ったHTMLを出力しました。描画中に発生する非同期処理の完了を待つには、ストリーミングかprerender系のAPIを使います。描画前にデータ取得を完了させていれば、renderToString でもデータ入りのHTMLを返せます。
キャッシュや再検証まで要るなら、取得専用のライブラリを検討します。@nanostores/query は npm registry の dist-tags.latest が2026年10月31日時点で0.3.4(2024-08-08公開)のままのため、TanStack Queryと機能を比べてから選びます。TanStack Queryの既定値の挙動はTanStack Queryとは?React Queryとの違い・v5の使い方と既定値の落とし穴で解説しています。
Astroでアイランド間の状態を共有する方法
Astroのページでは、Reactのコンポーネントを2つ置いても、それぞれが別々のアイランドとしてハイドレーションされます。共通の親が無いのでContextで値を渡せません。Astroのドキュメント「Share state between islands」は、この問題への答えとしてNano Storesを推奨しています。理由として挙げているのは、1KB未満で依存が無いことと、フレームワークを選ばないことの2点です。Astroそのものの特徴はAstroフレームワークとは?特徴と使い方・Next.jsとの使い分けを解説【2026年版】で扱っています。
ReactとVueのアイランドで同じストアを使う手順
ストアを1つのファイルに置き、各フレームワークの連携パッケージから読みます。ReactのボタンとVueの表示部品が、同じ $cart を参照する例です。
// src/stores/cart.ts
import { atom } from 'nanostores'
export const $cart = atom<string[]>([])
// src/components/AddButton.tsx(React)
import { $cart } from '../stores/cart'
export default function AddButton({ id }: { id: string }) {
return <button onClick={() => $cart.set([...$cart.get(), id])}>追加</button>
}
// src/components/CartCount.vue(Vue)
<script setup>
import { useStore } from '@nanostores/vue'
import { $cart } from '../stores/cart'
const cart = useStore($cart)
</script>
<template><span>{{ cart.length }} 件</span></template>
ページ側では両方に client:load などのクライアントディレクティブを付けます。ディレクティブが無いコンポーネントはサーバーでHTMLになるだけで、ストアの変更に反応しません。AstroでReactを動かす設定はAstroとReactの違いと使い分け|AstroでReactを使う方法【Astro 7対応】で扱っています。
.astroコンポーネントのscriptタグで共有する方法
フレームワークを使わない .astro コンポーネント同士でも、<script> タグの中でストアをimportすれば状態を共有できます。Astroのレシピ「Share state between Astro components」は、ボタン側で isOpen.set(true)、ダイアログ側で isOpen.subscribe() を呼んで表示を切り替える例を載せています。<script> はブラウザ用にバンドルされるので、同じモジュールのストアを参照します。
フロントマターでの利用を勧めない3つの制約
Astroのドキュメントは、サーバー側のフロントマターでNano Storesを使うことを勧めていません。挙げている制約は次の3つです。
.astroファイルやハイドレーションしないコンポーネントからストアに書き込んでも、クライアント側のコンポーネントが受け取る値は変わらない- ストアをpropsとしてクライアント側のコンポーネントに渡せない
- Astroコンポーネントは再描画されないので、
.astroファイルからストアの変更を購読できない
サーバーで決めた値をクライアントに渡したいときは、ストアではなくpropsで初期値を渡し、クライアント側で set() します。
@nanostores/persistentでlocalStorageに保存する方法
@nanostores/persistent は、ストアの値を localStorage に保存し、ブラウザのタブ間で同期する追加パッケージです。
persistentAtom・persistentJSON・persistentMapの使い分け
| 関数 | 保存の仕方 | 向く値 |
|---|---|---|
| persistentAtom | 1キーに文字列で保存 | 文字列 |
| persistentBoolean | 1キーに yes か空文字 | 設定のオンオフ |
| persistentJSON | 1キーにJSON文字列 | 配列・数値・オブジェクト |
| persistentMap | キーごとに別の項目 | 設定画面の項目群 |
import { persistentJSON, persistentMap } from '@nanostores/persistent'
export const $cart = persistentJSON<string[]>('cart', [])
export const $settings = persistentMap('settings:', {
theme: 'auto',
sidebar: 'show'
})
$settings.setKey('theme', 'dark')
persistentMap はキーの接頭辞を付けて項目ごとに保存します。setKey() 直後の localStorage には settings:theme だけがありましたが、続けて get() を呼ぶと mount 時の復元処理が走り、初期値の settings:sidebar も書き込まれました。保存キーの有無で「既定値のままか」を判定する処理は、読み取りや購読のタイミングで結果が変わるので避けます。persistentBoolean は true を文字列 yes、false を空文字として保存します。
数値が文字列に戻るpersistentAtomの落とし穴
persistentAtom は encode/decode を指定しないと、値を文字列として保存し、読み戻すときも文字列のまま返します。
const $count = persistentAtom('count', 0)
$count.set(6)
// 再読み込み後
$count.get() // "6"(文字列)
$count.get() + 1 // "61"
Node.js v26.5.0の --localstorage-file で確かめると、購読中に set(6) した値はそのページの中では数値の6のままでした。ところが、まだmountしていないか、購読解除後の猶予が過ぎてdisabledになった状態で set(6) してから get() すると、その get() でmount時の復元処理が走り、保存済みの文字列 "6" が返りました。再読み込み後は必ず文字列です。数値なら { encode: String, decode: Number }、配列やオブジェクトなら persistentJSON を使えば型が保たれます。保存先の容量やIndexedDBとの使い分けはIndexedDBのデメリットと容量上限|localStorage・Cookieとの違いと使い分けを参照してください。
タブ間同期とSSRでの扱い
既定では、別のタブで値が変わると storage イベント経由で自分のタブのストアも更新されます。{ listen: false } はこの通知の受け取りを止めるだけで、保存先は同じキーのままです。タブごとに別の下書きを持たせたい場合は setPersistentEngine() で保存先を sessionStorage などに差し替えます。
保存先は、読み込み時に typeof localStorage を調べて決まり、localStorage が無い環境では空のオブジェクトを使います。今回の検証環境であるNode.js v26.5.0では、保存ファイルを指定しない場合に localStorage が未定義になりました。この環境では、--localstorage-file を付けて起動すると有効になり、その中身は全リクエストで共有されます。SSRで利用者ごとの値を持たせる用途には使わず、サーバーでは初期値を描画する前提で組みます。
Zustand・Jotaiとの違いとバンドルサイズ実測
React専用のアプリでは、「軽いからNano Stores」は選ぶ理由になりにくいです。esbuild 0.28.2でminifyし、Node.jsのzlibで圧縮した最小構成のサイズは次のとおりでした(reactは外部化、単位はバイト)。READMEの公称値とは計測条件が異なります。この表は各APIをexportした最小エントリの比較で、画面・業務ロジック・永続化などを含むアプリ全体のサイズ比較ではありません。
| 構成 | minify | gzip | brotli |
|---|---|---|---|
| nanostores 1.5.5 atomのみ | 848 | 519 | 472 |
| nanostores atom+map+computed | 2,172 | 1,106 | 1,022 |
| nanostores atom+@nanostores/react | 1,721 | 975 | 903 |
| @nanostores/async computedAsync | 2,484 | 1,210 | 1,109 |
| zustand 5.0.16 create | 619 | 399 | 349 |
| jotai 3.0.1 atom+useAtom | 7,372 | 3,211 | 2,913 |
Reactから使う最小構成では、Zustandが349バイト、Nano Storesが903バイト、Jotaiが2,913バイトでした。どれも数KBに収まるので、サイズは決め手になりません。
3ライブラリの設計の違い
| 観点 | Nano Stores | Zustand | Jotai |
|---|---|---|---|
| 状態の単位 | 小さなストアを複数 | ストア内に状態と操作を定義 | atomを複数 |
| 対応UI | React・Vue・Svelteほか | React中心 | React |
| Provider | 不要 | 不要 | 任意 |
| 遅延初期化 | onMount | なし | onMount |
| ID別の状態 | mapCreator | 自前で実装 | atomFamily |
| 永続化 | 別パッケージ | persistミドルウェア | atomWithStorage |
Zustandの使い方はZustandとは?読み方・使い方とv5の注意点【5.0.16対応】、Jotaiのv3の変更点はJotaiとは?atomで組むReact状態管理の使い方とv3の変更点【3.0.1対応】で解説しています。
Nano Storesを選ぶ場面・選ばない場面
Nano Storesを選ぶべきなのは、AstroのアイランドのようにUIが複数のフレームワークや独立した部品に分かれている場合です。ストアの定義をUIから切り離せるので、ReactからPreactやReact Nativeへ移るときもストア側のコードはそのまま使えます。WebとFlutter / React Native開発のアプリで業務ロジックを共有する場合、Nano Storesのファイルをそのまま共通化できるのはJavaScriptを実行するReact Native側です。FlutterではDart側との連携設計が別途必要です。
逆に、React専用のSPAで、ミドルウェアを積み重ねて使うチームには向きません。デバッグ手段はREADMEのDevtoolsの節にある Redux DevTools連携(第三者製)、@nanostores/logger、@nanostores/vue のVue Devtoolsプラグインで、Zustandのように公式のミドルウェアとして揃ってはいません。Reactだけで完結し、Providerの範囲ごとに値を分けたい場合はJotai、Reactの外からも読みたい単一ストアならZustandのほうが手数が少なくなります。
よくある質問
nanostoresとNano Storesは同じものですか?
同じです。npmのパッケージ名とimport元が nanostores、READMEでの表記が「Nano Stores」です。連携パッケージは @nanostores/react のように @nanostores スコープで公開されています。
Nano Storesのサイズは何バイトですか?
README(1.5.5)の公称値はminifyとbrotli圧縮で351〜844バイトです。使う関数の数で変わり、esbuildでatomだけをバンドルした実測は472バイト、React連携込みで903バイトでした。
SvelteでNano Storesを使うのに連携パッケージは要りますか?
通常の .svelte ファイルなら要りません。Nano StoresのストアはSvelteのストア契約を満たしているため、$store と書けばそのまま購読できます。.svelte.ts ファイルやrunesのAPIで書く場合は、READMEが案内する @nanostores/svelte-runes の useStore() を使い、store.current で値を読みます。
Next.jsでもNano Storesを使えますか?
使えます。ストアはモジュール単位のシングルトンなので、サーバーではリクエストをまたいで値が共有されます。ユーザーごとの値をサーバーで set() すると別のリクエストに漏れるおそれがあるため、ユーザー固有の値はクライアント側で設定し、SSRでは ssr: 'initial' で初期値を描画します。
Reactでは状態管理にContextとNano Storesのどちらを使うべきですか?
テーマや認証情報のように変更が少なく、ツリーの一部だけに渡す値ならContextで足ります。頻繁に変わる値をContextに入れると、Providerの下にある利用側がまとめて再描画されます。値ごとに購読を分けたい場合や、Reactの外からも読み書きしたい場合にNano Storesが合います。Contextの書き方はReact Contextの使い方|createContext・useContext・Providerの実装と再レンダー対策【React 19.3対応】にまとめています。