---
title: "Nano Stores（nanostores）の使い方｜React・Astro連携と非同期・永続化の注意点【1.5.5対応】"
url: "https://www.issoh.co.jp/tech/details/9798/"
published: 2025-11-14
updated: 2026-10-31
categories: ["JavaScript"]
publisher: "株式会社一創"
---

# Nano Stores（nanostores）の使い方｜React・Astro連携と非同期・永続化の注意点【1.5.5対応】

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](https://github.com/nanostores/nanostores)は特徴を「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](https://www.npmjs.com/package/nanostores)の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](https://github.com/nanostores/nanostores/blob/main/CHANGELOG.md)から主な変更を拾いました。

| 版     | 変更                                         |
| ----- | ------------------------------------------ |
| 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', 'k@example.com') // 呼ばれる
```

実行すると、`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を生成すると、クライアントの初回描画と値が食い違い、[ハイドレーションエラー](/tech/details/3218/)の原因になります。[@nanostores/react](https://github.com/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](https://github.com/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](https://react.dev/reference/react-dom/static/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の使い方と既定値の落とし穴](/tech/details/10307/)で解説しています。

## Astroでアイランド間の状態を共有する方法

Astroのページでは、Reactのコンポーネントを2つ置いても、それぞれが別々のアイランドとしてハイドレーションされます。共通の親が無いのでContextで値を渡せません。[Astroのドキュメント「Share state between islands」](https://docs.astro.build/en/recipes/sharing-state-islands/)は、この問題への答えとしてNano Storesを推奨しています。理由として挙げているのは、1KB未満で依存が無いことと、フレームワークを選ばないことの2点です。Astroそのものの特徴は[Astroフレームワークとは？特徴と使い方・Next.jsとの使い分けを解説【2026年版】](/tech/details/15369/)で扱っています。

### 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対応】](/tech/details/2697/)で扱っています。

### .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](https://github.com/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との違いと使い分け](/tech/details/9666/)を参照してください。

### タブ間同期と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対応】](/tech/details/7615/)、Jotaiのv3の変更点は[Jotaiとは？atomで組むReact状態管理の使い方とv3の変更点【3.0.1対応】](/tech/details/7660/)で解説しています。

### Nano Storesを選ぶ場面・選ばない場面

Nano Storesを選ぶべきなのは、AstroのアイランドのようにUIが複数のフレームワークや独立した部品に分かれている場合です。ストアの定義をUIから切り離せるので、ReactからPreactやReact Nativeへ移るときもストア側のコードはそのまま使えます。Webと[Flutter / React Native開発](/service/apps/cross%5Fplatform/)のアプリで業務ロジックを共有する場合、Nano Storesのファイルをそのまま共通化できるのはJavaScriptを実行するReact Native側です。FlutterではDart側との連携設計が別途必要です。

逆に、React専用のSPAで、ミドルウェアを積み重ねて使うチームには向きません。デバッグ手段はREADMEのDevtoolsの節にある [Redux DevTools連携](https://nanostores-devtools.psdcoder.dev/)（第三者製）、@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対応】](/tech/details/2681/)にまとめています。

## 関連記事

- [Zustandとは？読み方・使い方とv5の注意点【5.0.16対応】](/tech/details/7615/)
- [Jotaiとは？atomで組むReact状態管理の使い方とv3の変更点【3.0.1対応】](/tech/details/7660/)
- [AstroとReactの違いと使い分け｜AstroでReactを使う方法【Astro 7対応】](/tech/details/2697/)
- [TanStack Queryとは？React Queryとの違い・v5の使い方と既定値の落とし穴](/tech/details/10307/)
- [IndexedDBのデメリットと容量上限｜localStorage・Cookieとの違いと使い分け](/tech/details/9666/)

---

出典: [Nano Stores（nanostores）の使い方｜React・Astro連携と非同期・永続化の注意点【1.5.5対応】](<https://www.issoh.co.jp/tech/details/9798/>)（株式会社一創）
