Jotai の使い方 — React のためのプリミティブで柔軟な状態管理ライブラリ
一言でいうと
Jotai は、React の useState のようなシンプルさを保ちながら、atom(アトム)という最小単位で状態を管理するボトムアップ型の状態管理ライブラリです。コアAPIはわずか2kbで、小規模なローカルステートから大規模なエンタープライズアプリケーションまでスケールします。
どんな時に使う?
- グローバルな状態を複数コンポーネントで共有したいが、Redux ほど大掛かりな仕組みは不要な時 — テーマ設定、認証状態、フィルター条件など、コンポーネントツリーの離れた場所で同じ状態を参照・更新するケース
- Context の再レンダリング問題を解消したい時 — React Context では Provider 配下の全コンポーネントが再レンダリングされがちですが、Jotai は atom 単位で購読するため、関係のないコンポーネントは再レンダリングされません
- 非同期データの取得結果を状態として扱いたい時 — API レスポンスを atom に格納し、React Suspense と組み合わせて宣言的にデータフェッチを行うケース
インストール
# npm
npm install jotai
# yarn
yarn add jotai
# pnpm
pnpm add jotai
前提条件: React 17.0.0 以上が必要です。React 18 以降で Suspense を活用する場合は Concurrent Features が利用可能な環境を推奨します。
基本的な使い方
最もよく使うパターンは「atom を定義して useAtom で読み書きする」というシンプルな流れです。
// atoms.ts
import { atom } from 'jotai'
export const countAtom = atom<number>(0)
// Counter.tsx
import { useAtom } from 'jotai'
import { countAtom } from './atoms'
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>+1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}
export default Counter
useAtom は useState とほぼ同じインターフェースです。違いは、どのコンポーネントから呼んでも同じ atom を参照すれば同じ状態を共有できるという点です。Provider なしでもデフォルトストアが使われるため、ラップ不要で動作します。
よく使うAPI — Jotai の主要APIの使い方
1. atom() — atom の作成
すべての起点となる関数です。プリミティブ値、オブジェクト、配列など何でも初期値に渡せます。
import { atom } from 'jotai'
// プリミティブ atom
const nameAtom = atom<string>('Taro')
// オブジェクト atom
const userAtom = atom<{ name: string; age: number }>({
name: 'Taro',
age: 30,
})
2. useAtom() — atom の読み書き
useState と同じタプル [value, setValue] を返します。
import { useAtom } from 'jotai'
function UserProfile() {
const [user, setUser] = useAtom(userAtom)
const handleBirthday = () => {
setUser((prev) => ({ ...prev, age: prev.age + 1 }))
}
return (
<div>
<p>{user.name} ({user.age}歳)</p>
<button onClick={handleBirthday}>誕生日🎂</button>
</div>
)
}
3. useAtomValue() / useSetAtom() — 読み取り専用・書き込み専用
再レンダリングの最適化に有効です。値を読むだけのコンポーネントでは useAtomValue、更新だけ行うコンポーネントでは useSetAtom を使うことで、不要な再レンダリングを防げます。
import { useAtomValue, useSetAtom } from 'jotai'
// 表示のみ — setCount の変更では再レンダリングされない
function CountDisplay() {
const count = useAtomValue(countAtom)
return <span>{count}</span>
}
// 更新のみ — count の値変更では再レンダリングされない
function CountButton() {
const setCount = useSetAtom(countAtom)
return <button onClick={() => setCount((c) => c + 1)}>+1</button>
}
4. 派生 atom(Derived Atom)— 読み取り専用 / 読み書き可能
既存の atom から計算値を導出できます。これが Jotai の最も強力な機能の一つです。
import { atom } from 'jotai'
const countAtom = atom<number>(0)
// 読み取り専用の派生 atom
const doubledAtom = atom<number>((get) => get(countAtom) * 2)
// 複数の atom を合成
const priceAtom = atom<number>(1000)
const taxRateAtom = atom<number>(0.1)
const totalPriceAtom = atom<number>(
(get) => get(priceAtom) * (1 + get(taxRateAtom))
)
// 読み書き可能な派生 atom
const countWithLogAtom = atom(
(get) => get(countAtom),
(get, set, newValue: number) => {
console.log(`count changed: ${get(countAtom)} -> ${newValue}`)
set(countAtom, newValue)
}
)
5. 非同期 atom — データフェッチとの統合
read 関数を async にすることで、非同期データを atom として扱えます。React Suspense と組み合わせて使います。
import { atom } from 'jotai'
import { Suspense } from 'react'
import { useAtomValue } from 'jotai'
interface Todo {
id: number
title: string
completed: boolean
}
const todoIdAtom = atom<number>(1)
const todoAtom = atom<Promise<Todo>>(async (get) => {
const id = get(todoIdAtom)
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`
)
return response.json()
})
function TodoItem() {
const todo = useAtomValue(todoAtom)
return (
<div>
<h3>{todo.title}</h3>
<p>{todo.completed ? '✅ 完了' : '⬜ 未完了'}</p>
</div>
)
}
function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<TodoItem />
</Suspense>
)
}
todoIdAtom を更新すると、todoAtom が自動的に再フェッチされ、コンポーネントが再レンダリングされます。
補足: Write-only atom(アクション atom)
read 関数を持たず、write 関数だけを定義するパターンです。複雑な更新ロジックをカプセル化するのに便利です。
const countAtom = atom<number>(0)
// 第1引数に null を渡すと write-only になる
const incrementByAtom = atom(null, (get, set, amount: number) => {
set(countAtom, get(countAtom) + amount)
})
function Controls() {
const [, incrementBy] = useAtom(incrementByAtom)
return (
<div>
<button onClick={() => incrementBy(1)}>+1</button>
<button onClick={() => incrementBy(10)}>+10</button>
</div>
)
}
類似パッケージとの比較
| 特徴 | Jotai | Zustand | Recoil | Redux Toolkit |
|---|---|---|---|---|
| アプローチ | ボトムアップ(atom 単位) | トップダウン(単一ストア) | ボトムアップ(atom 単位) | トップダウン(単一ストア) |
| バンドルサイズ | ~2kb | ~1.5kb | ~20kb | ~11kb |
| ボイラープレート | 極小 | 小 | 小 | 中 |
| TypeScript サポート | ◎(推論が優秀) | ◎ | ○ | ◎ |
| 文字列キーの要否 | 不要 | 不要 | 必要 | 不要 |
| React 外からのアクセス | store API で可能 | ネイティブ対応 | 不可 | ネイティブ対応 |
| 非同期処理 | Suspense 統合 | ミドルウェア | Suspense 統合 | RTK Query |
| メンテナンス状況 | 活発(pmndrs) | 活発(pmndrs) | 停滞気味 | 活発 |
| 学習コスト | 低 | 低 | 中 | 中〜高 |
選定の目安: コンポーネント単位で細かく状態を分割したいなら Jotai、単一ストアでシンプルに管理したいなら Zustand、大規模チームで厳格なフローが必要なら Redux Toolkit が向いています。Recoil は Meta のメンテナンスが停滞しているため、新規採用は慎重に検討してください。
注意点・Tips
1. atom はモジュールスコープで定義する
atom をコンポーネント内で定義すると、レンダリングのたびに新しい atom が生成されてしまいます。必ずコンポーネントの外(モジュールスコープ)で定義してください。
// ❌ NG: レンダリングのたびに新しい atom が作られる
function Counter() {
const countAtom = atom(0)
const [count, setCount] = useAtom(countAtom)
// ...
}
// ✅ OK: モジュールスコープで定義
const countAtom = atom(0)
function Counter() {
const [count, setCount] = useAtom(countAtom)
// ...
}
2. useAtomValue と useSetAtom を積極的に使い分ける
useAtom は値と setter の両方を返すため、値が変わると必ず再レンダリングされます。更新しか行わないコンポーネントでは useSetAtom を使うことで、パフォーマンスを改善できます。
3. Provider の使い分け
Jotai はデフォルトで Provider なしで動作しますが、Provider を使うとスコープを分離できます。テストやストーリーブックで独立した状態を持たせたい場合に有効です。
import { Provider } from 'jotai'
function App() {
return (
<Provider>
<Counter />
</Provider>
)
}
4. 公式ユーティリティを活用する
Jotai には jotai/utils に便利なユーティリティが多数用意されています。
import { atomWithStorage } from 'jotai/utils'
// localStorage と自動同期する atom
const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')
他にも atomWithReducer、atomFamily、selectAtom、splitAtom など、実践で頻出するパターンが揃っています。
5. DevTools でデバッグする
debugLabel を設定すると、React DevTools や Jotai DevTools で atom を識別しやすくなります。
const countAtom = atom(0)
countAtom.debugLabel = 'countAtom'
6. 非同期 atom には Suspense が必要
非同期の read 関数を持つ atom を useAtom / useAtomValue で読み取る場合、上位に <Suspense> が必要です。忘れるとエラーになるので注意してください。
まとめ
Jotai は「atom を作って useAtom で使う」というシンプルな原則だけで、React の状態管理を驚くほど簡潔に実現できるライブラリです。派生 atom による計算値の導出、非同期データの Suspense 統合、jotai/utils の豊富なユーティリティにより、小規模から大規模まで無理なくスケールします。React の useState に物足りなさを感じたら、まず Jotai を試してみることをおすすめします。