はじめに
状態管理ライブラリ、正直なところ選択肢が多すぎて迷いますよね。Redux、Zustand、Jotai、Recoil...。個人的にはZustandをよく使っていたんですが、最近「Nano Stores」というライブラリを知って、これがなかなか良いんですよ。
何が良いかって、まず286バイトという驚異的な軽さ。しかもReact、Vue、Svelteなど複数のフレームワークで使える。GitHubスターも約7,000近くあって、実績も十分です。
今回は、このNano Storesについて基本的な使い方から実践的なユースケースまで解説していきます。
特徴・メリット
1. 圧倒的な軽さ
Nano Storesの最大の特徴は、そのサイズ感です。
- コアサイズ: 286バイト(minified + brotli)
- 依存関係: ゼロ
- バンドルサイズ全体: 約2KB
これ、意外と重要な話で、状態管理ライブラリって地味にバンドルサイズを食うんですよね。特にモバイル向けのWebアプリだと、この差が体感速度に影響してきます。
2. フレームワーク非依存
対応フレームワークが幅広いのも魅力です。
- React / React Native
- Vue
- Svelte
- Preact
- Solid
- バニラJS
個人的には、この「フレームワーク非依存」という設計思想が気に入っています。プロジェクトによってReactだったりVueだったりするわけですが、状態管理のロジックを共通化できるのは大きい。
3. TypeScriptファースト
型サポートが優秀なんですよ。後付けではなく、最初からTypeScriptで書かれているので、型推論がしっかり効きます。30代になって思うのは、型安全性って本当に大事だということ。バグを事前に防げるのはQOL上がりますね。
4. ツリーシェイク対応
使っていない機能はバンドルに含まれません。これ当たり前のようで、意外とできていないライブラリも多い。Nano Storesは設計段階からこれを意識しているので、本当に必要な分だけがビルドに含まれます。
インストール方法
インストールは非常にシンプルです。
npm install nanostores
フレームワーク用のバインディングも必要に応じて追加します。
# React用
npm install nanostores @nanostores/react
# Vue用
npm install nanostores @nanostores/vue
# Svelte用(Svelte 5対応)
npm install nanostores @nanostores/svelte
基本的な使い方
Atom(アトム)
最もシンプルなストアです。文字列や数値など、単一の値を管理します。
// stores/counter.ts
import { atom } from 'nanostores'
export const $counter = atom(0)
// 値の更新
$counter.set(1)
// 値の取得
console.log($counter.get()) // 1
変数名の先頭に$をつけるのが慣習になっています。これ、最初は違和感ありましたが、「これはストアだ」と一目でわかるので、コードの可読性は上がりますね。
Map(マップ)
オブジェクトを管理する場合はMapを使います。
// stores/user.ts
import { map } from 'nanostores'
export const $user = map({
name: '',
email: '',
isLoggedIn: false
})
// 特定のキーだけ更新
$user.setKey('name', 'Takuma')
$user.setKey('isLoggedIn', true)
// 全体を更新
$user.set({
name: 'Takuma',
email: 'takuma@example.com',
isLoggedIn: true
})
setKeyで個別のプロパティを更新できるのが便利です。
Computed(計算ストア)
他のストアの値から派生する値を定義できます。
// stores/users.ts
import { atom, computed } from 'nanostores'
interface User {
id: number
name: string
isAdmin: boolean
}
export const $users = atom<User[]>([])
// 管理者のみをフィルタリング
export const $admins = computed($users, users =>
users.filter(user => user.isAdmin)
)
// ユーザー数
export const $userCount = computed($users, users => users.length)
この辺りはRecoilのselectorやZustandの派生状態に近い感覚で使えます。
Reactでの使用例
Reactで使う場合は@nanostores/reactのuseStoreフックを使います。
// components/Counter.tsx
import { useStore } from '@nanostores/react'
import { $counter } from '../stores/counter'
export function Counter() {
const counter = useStore($counter)
return (
<div>
<p>Count: {counter}</p>
<button onClick={() => $counter.set(counter + 1)}>
Increment
</button>
</div>
)
}
シンプルですよね。Zustandに慣れている人なら、ほぼ同じ感覚で使えると思います。
Vueでの使用例
<script setup lang="ts">
import { useStore } from '@nanostores/vue'
import { $counter } from '../stores/counter'
const counter = useStore($counter)
</script>
<template>
<div>
<p>Count: {{ counter }}</p>
<button @click="$counter.set(counter + 1)">
Increment
</button>
</div>
</template>
実践的なユースケース
認証状態の管理
実務でよくある認証状態の管理を例にしてみます。
// stores/auth.ts
import { atom, computed, map } from 'nanostores'
interface AuthUser {
id: string
name: string
email: string
role: 'admin' | 'user'
}
// 認証状態
export const $authUser = atom<AuthUser | null>(null)
// ログイン状態の派生
export const $isLoggedIn = computed(
$authUser,
user => user !== null
)
// 管理者かどうか
export const $isAdmin = computed(
$authUser,
user => user?.role === 'admin'
)
// ログイン処理
export async function login(email: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
const user = await response.json()
$authUser.set(user)
}
// ログアウト処理
export function logout() {
$authUser.set(null)
}
ストアにロジックを寄せることで、コンポーネントがスッキリします。これ、Nano Storesの設計思想でもある「ロジックをコンポーネントからストアへ移動する」という考え方ですね。
フォーム状態の管理
フォームの状態管理もMapを使えば簡単です。
// stores/contactForm.ts
import { map, computed } from 'nanostores'
export const $contactForm = map({
name: '',
email: '',
message: ''
})
export const $formErrors = map({
name: '',
email: '',
message: ''
})
// バリデーション状態
export const $isFormValid = computed(
[$contactForm, $formErrors],
(form, errors) => {
const hasValues = form.name && form.email && form.message
const hasNoErrors = !errors.name && !errors.email && !errors.message
return hasValues && hasNoErrors
}
)
// バリデーション関数
export function validateEmail(email: string) {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
$formErrors.setKey('email', isValid ? '' : 'メールアドレスの形式が正しくありません')
}
複数ストアの購読(Effect)
複数のストアの変更を監視して副作用を実行したい場合は、effectを使います。
import { effect } from 'nanostores'
import { $authUser } from './auth'
import { $settings } from './settings'
// 認証状態や設定が変わったらログを送信
effect([$authUser, $settings], () => {
const user = $authUser.get()
const settings = $settings.get()
if (user) {
analytics.identify(user.id, {
theme: settings.theme,
notifications: settings.notifications
})
}
})
まとめ
Nano Storesは「軽量」「フレームワーク非依存」「TypeScriptファースト」という3つの特徴を持った状態管理ライブラリです。
個人的には、以下のようなケースで特にオススメです。
- 軽量さを重視するプロジェクト: モバイルWebやパフォーマンス重視のサイト
- 複数フレームワークを使う環境: マイクロフロントエンドや、ReactとVueが混在するプロジェクト
- シンプルな状態管理で十分なケース: 大規模なReduxは必要ないけど、Context APIだと物足りない場合
正直なところ、大規模アプリでReduxの代替として使うには機能が足りない面もあります。ただ、中小規模のプロジェクトや、状態管理をシンプルに保ちたい場合は、Nano Stores一択ですね。
286バイトという軽さで、これだけの機能が使えるのは素直にすごいと思います。興味があれば、ぜひ試してみてください。