Jotai完全ガイド 2025 - アトミック状態管理の新時代
Jotai v2を徹底解説。アトミックアプローチによる状態管理の革新、primitive・derived atoms、React 18対応、TypeScript完全サポート、実践的なパターンまで、モダンなReactアプリケーション開発の全てを網羅します。
JotaiはReactアプリケーションにおけるステート管理の新しいアプローチを提供する軽量ライブラリです。原子的なステート管理によりパフォーマンスを最適化し、Meta、Adobe、TikTokなどの企業で採用されています。
Reactのステート管理は長年の課題でしたが、2025 年現在、Jotai が新しいスタンダードとして注目を集めています。Recoil が Meta 社によってアーカイブ化された今、Jotai は原子的(Atomic)なアプローチでステート管理を革新し、Meta、Adobe、TikTok、Uniswap などの大手企業で採用されています。
Jotai は「原子的(Atomic)なアプローチ」による Reactステート管理ライブラリです。従来の Redux や Zustand とは異なり、小さなステートの単位(atom)を組み合わせて複雑なステートを構築します。
特徴 | Jotai | 従来のライブラリ | 利点 |
---|---|---|---|
学習コスト | 低い | 高い | useStateライクなAPI |
バンドルサイズ | 2.4KB | 5-15KB | 軽量で高速 |
再レンダリング | 最適化済み | 手動最適化が必要 | 自動で最適化 |
TypeScript | フル対応 | 部分対応 | 型安全性が高い |
デバッグ | React DevTools | 専用ツール | React標準ツールで可能 |
チャートを読み込み中...
import { atom, useAtom } from 'jotai'
// 基本的なatom
const countAtom = atom(0)
// 派生atom(derived atom)
const doubleCountAtom = atom((get) => get(countAtom) * 2)
// コンポーネントでの使用
function Counter() {
const [count, setCount] = useAtom(countAtom)
const [doubleCount] = useAtom(doubleCountAtom)
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
)
}
// 複数のコンポーネント間でのステート共有が困難
function App() {
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
// Context経由で共有するか、props drilling が必要
return (
<UserContext.Provider value={{ user, setUser }}>
<PostsContext.Provider value={{ posts, setPosts }}>
<UserProfile />
<PostsList />
</PostsContext.Provider>
</UserContext.Provider>
)
}
// atomで自然にステートを共有
const userAtom = atom(null)
const postsAtom = atom([])
function UserProfile() {
const [user] = useAtom(userAtom)
return <div>{user?.name}</div>
}
function PostsList() {
const [posts] = useAtom(postsAtom)
return <ul>{posts.map(post => <li key={post.id}>{post.title}</li>)}</ul>
}
// 複数のコンポーネント間でのステート共有が困難
function App() {
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
// Context経由で共有するか、props drilling が必要
return (
<UserContext.Provider value={{ user, setUser }}>
<PostsContext.Provider value={{ posts, setPosts }}>
<UserProfile />
<PostsList />
</PostsContext.Provider>
</UserContext.Provider>
)
}
// atomで自然にステートを共有
const userAtom = atom(null)
const postsAtom = atom([])
function UserProfile() {
const [user] = useAtom(userAtom)
return <div>{user?.name}</div>
}
function PostsList() {
const [posts] = useAtom(postsAtom)
return <ul>{posts.map(post => <li key={post.id}>{post.title}</li>)}</ul>
}
import { atom } from 'jotai'
// 非同期データ取得のatom
const userAtom = atom(async (get) => {
const userId = get(userIdAtom)
if (!userId) return null
const response = await fetch(`/api/users/${userId}`)
return response.json()
})
// ローディングステートの管理
const userLoadingAtom = atom((get) => {
try {
get(userAtom)
return false
} catch (promise) {
return true
}
})
function UserProfile() {
const [user] = useAtom(userAtom)
const [loading] = useAtom(userLoadingAtom)
if (loading) return <div>Loading...</div>
if (!user) return <div>No user found</div>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
// LocalStorageに自動保存されるatom
const themeAtom = atomWithStorage('theme', 'light')
// SessionStorageを使用
const tempDataAtom = atomWithStorage('tempData', {}, sessionStorage)
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom)
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
)
}
import { atom } from 'jotai'
const postsAtom = atom([])
const addPostAtom = atom(
null,
async (get, set, newPost) => {
// 楽観的更新:即座にUIを更新
const currentPosts = get(postsAtom)
const optimisticPost = { ...newPost, id: Date.now(), pending: true }
set(postsAtom, [...currentPosts, optimisticPost])
try {
// サーバーに送信
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost),
headers: { 'Content-Type': 'application/json' }
})
const savedPost = await response.json()
// 成功時:楽観的更新を実際のデータで置換
set(postsAtom, prev =>
prev.map(post =>
post.id === optimisticPost.id ? savedPost : post
)
)
} catch (error) {
// 失敗時:楽観的更新を削除
set(postsAtom, prev =>
prev.filter(post => post.id !== optimisticPost.id)
)
throw error
}
}
)
過度に大きなオブジェクトを単一の atom で管理するのではなく、関連する小さな単位で atom を分割することで、不要な再レンダリングを防げます。
// アンチパターン:大きなオブジェクトの単一管理
const appStateAtom = atom({
user: null,
posts: [],
comments: [],
notifications: [],
settings: {}
})
// 一部の更新で全コンポーネントが再レンダリング
function updateUser(user) {
setAppState(prev => ({ ...prev, user }))
}
// ベストプラクティス:関連する単位で分割
const userAtom = atom(null)
const postsAtom = atom([])
const commentsAtom = atom([])
const notificationsAtom = atom([])
const settingsAtom = atom({})
// 必要な部分のみが再レンダリング
function updateUser(user) {
setUser(user) // userAtomを使用するコンポーネントのみ更新
}
// アンチパターン:大きなオブジェクトの単一管理
const appStateAtom = atom({
user: null,
posts: [],
comments: [],
notifications: [],
settings: {}
})
// 一部の更新で全コンポーネントが再レンダリング
function updateUser(user) {
setAppState(prev => ({ ...prev, user }))
}
// ベストプラクティス:関連する単位で分割
const userAtom = atom(null)
const postsAtom = atom([])
const commentsAtom = atom([])
const notificationsAtom = atom([])
const settingsAtom = atom({})
// 必要な部分のみが再レンダリング
function updateUser(user) {
setUser(user) // userAtomを使用するコンポーネントのみ更新
}
import { Suspense } from 'react'
import { atom, useAtom } from 'jotai'
const asyncDataAtom = atom(async () => {
const response = await fetch('/api/data')
return response.json()
})
function DataComponent() {
const [data] = useAtom(asyncDataAtom)
return <div>{JSON.stringify(data)}</div>
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
)
}
Meta社がRecoilをアーカイブ化し、新規プロジェクトでの使用を非推奨に
Recoilユーザーの多くがJotaiに移行開始
import { atom, selector, useRecoilState } from 'recoil'
const countState = atom({
key: 'countState',
default: 0,
})
const doubleCountState = selector({
key: 'doubleCountState',
get: ({get}) => {
const count = get(countState)
return count * 2
},
})
function Counter() {
const [count, setCount] = useRecoilState(countState)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const doubleCountAtom = atom((get) => {
const count = get(countAtom)
return count * 2
})
function Counter() {
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
import { atom, selector, useRecoilState } from 'recoil'
const countState = atom({
key: 'countState',
default: 0,
})
const doubleCountState = selector({
key: 'doubleCountState',
get: ({get}) => {
const count = get(countState)
return count * 2
},
})
function Counter() {
const [count, setCount] = useRecoilState(countState)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const doubleCountAtom = atom((get) => {
const count = get(countAtom)
return count * 2
})
function Counter() {
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
機能 | Redux Toolkit | Jotai | 移行の複雑さ |
---|---|---|---|
基本的なステート | createSlice | atom | 簡単 |
非同期処理 | createAsyncThunk | async atom | 簡単 |
派生ステート | createSelector | derived atom | 簡単 |
ミドルウェア | middleware | atom effects | 中程度 |
開発ツール | Redux DevTools | React DevTools | 簡単 |
Jotai は Reactの哲学に最も近いステート管理ライブラリの 1 つです。原子的なアプローチにより、コンポーネントの再利用性と保守性が大幅に向上します。
Jotai は 2025 年の Reactステート管理における最有力な選択肢です:
従来のステート管理ライブラリからの移行も比較的簡単で、段階的な導入が可能です。新規プロジェクトではもちろん、既存プロジェクトでも部分的な導入から始めることをお勧めします。