Zustand vs Jotai徹底比較2026 - アトミックvsストア、パフォーマンス、使い分け、実装例完全ガイド
Zustand vs Jotai徹底比較2026
ZustandとJotaiは、Reactの軽量状態管理ライブラリです。本記事では、両者の違いと使い分けを実例付きで徹底解説します。
目次
- ZustandとJotaiの概要
- アーキテクチャの違い
- 基本的な使い方
- パフォーマンス比較
- DevToolsとデバッグ
- 実践パターン
- 使い分けガイド
- マイグレーション
ZustandとJotaiの概要
Zustand
/**
* Zustand の特徴
*
* 1. ストアベース
* - Redux風の単一ストア
* - シンプルなAPI
*
* 2. Hooks不要
* - コンポーネント外でも使用可能
*
* 3. ミドルウェア
* - persist、devtools等
*
* 4. バンドルサイズ
* - 約1KB(gzip)
*/
import { create } from 'zustand'
// Zustandストアの作成
interface CounterStore {
count: number
increment: () => void
decrement: () => void
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}))
// 使用例
function Counter() {
const { count, increment, decrement } = useCounterStore()
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
Jotai
/**
* Jotai の特徴
*
* 1. アトミックベース
* - Recoil風の分散状態
* - ボトムアップ設計
*
* 2. 宣言的
* - 派生状態が自然
*
* 3. TypeScript完全対応
* - 型推論が強力
*
* 4. バンドルサイズ
* - 約2KB(gzip)
*/
import { atom, useAtom } from 'jotai'
// Jotaiアトムの作成
const countAtom = atom(0)
const incrementAtom = atom(
(get) => get(countAtom),
(get, set) => set(countAtom, get(countAtom) + 1)
)
const decrementAtom = atom(
(get) => get(countAtom),
(get, set) => set(countAtom, get(countAtom) - 1)
)
// 使用例
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
)
}
アーキテクチャの違い
データフロー
// Zustand: トップダウン(ストアから各コンポーネントへ)
//
// ┌─────────┐
// │ Store │
// └────┬────┘
// ┌────┴────┐
// │ │
// ┌───▼───┐ ┌──▼────┐
// │ Comp1 │ │ Comp2 │
// └───────┘ └───────┘
const useStore = create((set) => ({
user: null,
posts: [],
comments: [],
// すべての状態を一箇所で管理
}))
// Jotai: ボトムアップ(アトムを組み合わせて状態を構築)
//
// ┌──────┐ ┌──────┐ ┌──────┐
// │Atom1 │ │Atom2 │ │Atom3 │
// └───┬──┘ └───┬──┘ └───┬──┘
// │ │ │
// └────┬────┴────┬────┘
// │ │
// ┌───▼───┐ ┌──▼────┐
// │ Comp1 │ │ Comp2 │
// └───────┘ └───────┘
const userAtom = atom(null)
const postsAtom = atom([])
const commentsAtom = atom([])
// 各状態が独立したアトム
状態の更新
// Zustand: immer風の更新
const useStore = create<State>((set) => ({
user: null,
setUser: (user) => set({ user }),
updateUserName: (name) =>
set((state) => ({
user: state.user ? { ...state.user, name } : null
}))
}))
// Jotai: ReactのuseStateと同じ感覚
const userAtom = atom<User | null>(null)
function Component() {
const [user, setUser] = useAtom(userAtom)
// 直接更新
setUser({ id: '1', name: 'John' })
// 関数更新
setUser((prev) => prev ? { ...prev, name: 'Jane' } : null)
}
基本的な使い方
Zustandの基本パターン
// 1. シンプルなストア
interface TodoStore {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: string) => void
removeTodo: (id: string) => void
}
const useTodoStore = create<TodoStore>((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: crypto.randomUUID(), text, done: false }]
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id)
}))
}))
// 2. セレクターで最適化
function TodoList() {
// 必要な状態だけ購読
const todos = useTodoStore((state) => state.todos)
const toggleTodo = useTodoStore((state) => state.toggleTodo)
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
)
}
// 3. 複数ストアの組み合わせ
const useAuthStore = create<AuthStore>((set) => ({
user: null,
login: async (credentials) => {
const user = await api.login(credentials)
set({ user })
},
logout: () => set({ user: null })
}))
const useCartStore = create<CartStore>((set) => ({
items: [],
addItem: (item) =>
set((state) => ({
items: [...state.items, item]
}))
}))
function App() {
const user = useAuthStore((state) => state.user)
const items = useCartStore((state) => state.items)
return <div>{/* ... */}</div>
}
Jotaiの基本パターン
// 1. プリミティブアトム
const todosAtom = atom<Todo[]>([])
// 2. 派生アトム(読み取り専用)
const completedTodosAtom = atom((get) => {
const todos = get(todosAtom)
return todos.filter((todo) => todo.done)
})
const todoStatsAtom = atom((get) => {
const todos = get(todosAtom)
return {
total: todos.length,
completed: todos.filter((t) => t.done).length,
active: todos.filter((t) => !t.done).length
}
})
// 3. 読み書きアトム
const addTodoAtom = atom(
null,
(get, set, text: string) => {
const newTodo: Todo = {
id: crypto.randomUUID(),
text,
done: false
}
set(todosAtom, [...get(todosAtom), newTodo])
}
)
const toggleTodoAtom = atom(
null,
(get, set, id: string) => {
set(
todosAtom,
get(todosAtom).map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
)
}
)
// 4. 使用例
function TodoList() {
const [todos] = useAtom(todosAtom)
const [, addTodo] = useAtom(addTodoAtom)
const [, toggleTodo] = useAtom(toggleTodoAtom)
const [stats] = useAtom(todoStatsAtom)
return (
<div>
<p>Total: {stats.total}, Completed: {stats.completed}</p>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
</div>
)
}
非同期処理
// Zustand
const useUserStore = create<UserStore>((set) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id) => {
set({ loading: true, error: null })
try {
const user = await api.getUser(id)
set({ user, loading: false })
} catch (error) {
set({ error: error.message, loading: false })
}
}
}))
// Jotai
const userIdAtom = atom<string | null>(null)
const userAtom = atom(async (get) => {
const userId = get(userIdAtom)
if (!userId) return null
return api.getUser(userId)
})
function UserProfile() {
const [userId, setUserId] = useAtom(userIdAtom)
const [user] = useAtom(userAtom) // 自動的にSuspense対応
return (
<Suspense fallback={<div>Loading...</div>}>
<div>{user?.name}</div>
</Suspense>
)
}
パフォーマンス比較
レンダリング最適化
// Zustand: セレクターによる最適化
function TodoItem({ id }: { id: string }) {
// この todo が変更された時だけ再レンダリング
const todo = useTodoStore(
(state) => state.todos.find((t) => t.id === id),
shallow // 浅い比較
)
return <div>{todo?.text}</div>
}
// Jotai: アトムの粒度による最適化
const todoAtom = atomFamily((id: string) =>
atom((get) => {
const todos = get(todosAtom)
return todos.find((t) => t.id === id)
})
)
function TodoItem({ id }: { id: string }) {
// この todo が変更された時だけ再レンダリング
const [todo] = useAtom(todoAtom(id))
return <div>{todo?.text}</div>
}
atomFamilyパターン
// Jotai: 動的なアトム生成
import { atomFamily } from 'jotai/utils'
const todoAtomFamily = atomFamily((id: string) =>
atom(
(get) => get(todosAtom).find((t) => t.id === id),
(get, set, update: Partial<Todo>) => {
set(
todosAtom,
get(todosAtom).map((t) =>
t.id === id ? { ...t, ...update } : t
)
)
}
)
)
function TodoItem({ id }: { id: string }) {
const [todo, updateTodo] = useAtom(todoAtomFamily(id))
return (
<div>
<input
type="checkbox"
checked={todo?.done}
onChange={(e) => updateTodo({ done: e.target.checked })}
/>
{todo?.text}
</div>
)
}
// Zustand: スライスパターンで近い効果
const createTodoSlice = (id: string) => (set: SetState, get: GetState) => ({
[`todo_${id}`]: null as Todo | null,
[`updateTodo_${id}`]: (update: Partial<Todo>) =>
set((state) => ({
[`todo_${id}`]: { ...state[`todo_${id}`], ...update }
}))
})
DevToolsとデバッグ
Zustand DevTools
import { devtools } from 'zustand/middleware'
const useStore = create<Store>()(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
decrement: () => set((state) => ({ count: state.count - 1 }), false, 'decrement')
}),
{ name: 'CounterStore' }
)
)
// Redux DevToolsで状態を可視化
Jotai DevTools
import { useAtomDevtools } from 'jotai-devtools'
function DebugAtoms() {
useAtomDevtools(countAtom, { name: 'count' })
useAtomDevtools(userAtom, { name: 'user' })
return null
}
// または専用のDevToolsパネル
import { DevTools } from 'jotai-devtools'
function App() {
return (
<>
<DevTools />
<YourApp />
</>
)
}
ロギング
// Zustand: ミドルウェアでロギング
const log = (config) => (set, get, api) =>
config(
(...args) => {
console.log('Before:', get())
set(...args)
console.log('After:', get())
},
get,
api
)
const useStore = create<Store>()(
log((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}))
)
// Jotai: カスタムフック
function useAtomWithLog<T>(anAtom: Atom<T>) {
const [value, setValue] = useAtom(anAtom)
const setValueWithLog = useCallback((update: SetStateAction<T>) => {
console.log('Before:', value)
setValue(update)
console.log('After:', update)
}, [value, setValue])
return [value, setValueWithLog] as const
}
実践パターン
ECサイトのカート
// Zustand実装
interface CartStore {
items: CartItem[]
addItem: (product: Product) => void
removeItem: (productId: string) => void
updateQuantity: (productId: string, quantity: number) => void
clear: () => void
total: number
}
const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (product) =>
set((state) => {
const existing = state.items.find((item) => item.product.id === product.id)
if (existing) {
return {
items: state.items.map((item) =>
item.product.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
}
return {
items: [...state.items, { product, quantity: 1 }]
}
}),
removeItem: (productId) =>
set((state) => ({
items: state.items.filter((item) => item.product.id !== productId)
})),
updateQuantity: (productId, quantity) =>
set((state) => ({
items: state.items.map((item) =>
item.product.id === productId ? { ...item, quantity } : item
)
})),
clear: () => set({ items: [] }),
get total() {
return get().items.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0
)
}
}))
// Jotai実装
const cartItemsAtom = atom<CartItem[]>([])
const addItemAtom = atom(null, (get, set, product: Product) => {
const items = get(cartItemsAtom)
const existing = items.find((item) => item.product.id === product.id)
if (existing) {
set(
cartItemsAtom,
items.map((item) =>
item.product.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
)
} else {
set(cartItemsAtom, [...items, { product, quantity: 1 }])
}
})
const cartTotalAtom = atom((get) => {
const items = get(cartItemsAtom)
return items.reduce((sum, item) => sum + item.product.price * item.quantity, 0)
})
フォーム管理
// Zustand
interface FormStore {
values: Record<string, any>
errors: Record<string, string>
touched: Record<string, boolean>
setFieldValue: (field: string, value: any) => void
setFieldError: (field: string, error: string) => void
setFieldTouched: (field: string) => void
reset: () => void
}
const useFormStore = create<FormStore>((set) => ({
values: {},
errors: {},
touched: {},
setFieldValue: (field, value) =>
set((state) => ({
values: { ...state.values, [field]: value }
})),
setFieldError: (field, error) =>
set((state) => ({
errors: { ...state.errors, [field]: error }
})),
setFieldTouched: (field) =>
set((state) => ({
touched: { ...state.touched, [field]: true }
})),
reset: () => set({ values: {}, errors: {}, touched: {} })
}))
// Jotai
const formValuesAtom = atom<Record<string, any>>({})
const formErrorsAtom = atom<Record<string, string>>({})
const formTouchedAtom = atom<Record<string, boolean>>({})
const fieldAtomFamily = atomFamily((field: string) =>
atom(
(get) => get(formValuesAtom)[field],
(get, set, value: any) => {
set(formValuesAtom, { ...get(formValuesAtom), [field]: value })
}
)
)
使い分けガイド
Zustandが向いているケース
/**
* Zustandを選ぶべきケース
*
* 1. グローバルな状態管理
* - 認証、テーマ、通知等
*
* 2. アクションが多い
* - 複雑なビジネスロジック
*
* 3. ミドルウェアが必要
* - persist、devtools等
*
* 4. Redux経験者
* - 似たような設計思想
*/
// 認証状態の管理
const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
user: null,
token: null,
login: async (credentials) => {
const { user, token } = await api.login(credentials)
set({ user, token })
},
logout: () => set({ user: null, token: null }),
refreshToken: async () => {
const token = await api.refreshToken()
set({ token })
}
}),
{ name: 'auth-storage' }
)
)
Jotaiが向いているケース
/**
* Jotaiを選ぶべきケース
*
* 1. 派生状態が多い
* - フィルター、ソート、集計等
*
* 2. 状態が分散している
* - 各コンポーネントが独立した状態
*
* 3. Suspense/Concurrent Mode
* - React 18の機能をフル活用
*
* 4. 型安全性重視
* - TypeScriptの型推論が強力
*/
// フィルター可能なリスト
const itemsAtom = atom<Item[]>([])
const filterAtom = atom('')
const sortByAtom = atom<'name' | 'date'>('name')
const filteredItemsAtom = atom((get) => {
const items = get(itemsAtom)
const filter = get(filterAtom)
const sortBy = get(sortByAtom)
return items
.filter((item) => item.name.includes(filter))
.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name)
}
return a.date.getTime() - b.date.getTime()
})
})
マイグレーション
ZustandからJotaiへ
// Before: Zustand
const useStore = create<Store>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}))
function Component() {
const { count, increment } = useStore()
return <button onClick={increment}>{count}</button>
}
// After: Jotai
const countAtom = atom(0)
const incrementAtom = atom(null, (get, set) => {
set(countAtom, get(countAtom) + 1)
})
function Component() {
const [count] = useAtom(countAtom)
const [, increment] = useAtom(incrementAtom)
return <button onClick={increment}>{count}</button>
}
JotaiからZustandへ
// Before: Jotai
const countAtom = atom(0)
const doubleCountAtom = atom((get) => get(countAtom) * 2)
function Component() {
const [count, setCount] = useAtom(countAtom)
const [doubleCount] = useAtom(doubleCountAtom)
return <div>{count} / {doubleCount}</div>
}
// After: Zustand
const useStore = create<Store>((set, get) => ({
count: 0,
get doubleCount() {
return get().count * 2
},
setCount: (count) => set({ count })
}))
function Component() {
const count = useStore((state) => state.count)
const doubleCount = useStore((state) => state.doubleCount)
const setCount = useStore((state) => state.setCount)
return <div>{count} / {doubleCount}</div>
}
まとめ
ZustandとJotaiは、それぞれ異なる哲学を持つ優れた状態管理ライブラリです。
Zustand:
- ストアベース、トップダウン設計
- シンプルで分かりやすい
- ミドルウェアが豊富
- Redux経験者に馴染みやすい
Jotai:
- アトミックベース、ボトムアップ設計
- 派生状態が自然
- Suspense/Concurrent Mode対応
- 型安全性が高い
選択基準:
- グローバルな状態が多い → Zustand
- 派生状態・分散した状態が多い → Jotai
- ミドルウェアが必要 → Zustand
- React 18の機能を活用 → Jotai
プロジェクトの要件に応じて、適切なライブラリを選択しましょう。