Reactの状態管理は、かつてRedux一択の時代から大きく変化しました。2026年現在、軽量で直感的なZustandやJotai、サーバー状態に特化したTanStack Queryなど、目的に応じた選択肢が充実しています。
本記事では、これらの主要ライブラリの特徴と使い方を比較し、プロジェクトの要件に応じた最適な選び方を解説します。
React状態管理の現在地
状態管理を考える上で、まず「状態の種類」を整理することが重要です。
状態の分類
UIステート(クライアント状態):モーダルの開閉、フォーム入力値、テーマ設定など、クライアント側で完結する状態です。
サーバーステート:APIから取得したデータ、キャッシュ、ローディング状態、エラー状態など、サーバーとの同期が必要な状態です。
URLステート:ページネーション、フィルター、ソート条件など、URLパラメータに反映されるべき状態です。
かつてのReduxはこれらすべてを1つのストアで管理しようとしましたが、現在は状態の種類に応じて適切なツールを使い分けるのが主流です。
2026年の主要な選択肢
Zustand:シンプルなグローバル状態管理。小〜中規模のクライアント状態に最適。
Jotai:アトミックな状態管理。きめ細かいレンダリング最適化が特徴。
TanStack Query(React Query):サーバー状態の管理に特化。キャッシュ、再取得、楽観的更新を自動化。
React標準(useState/useContext):小規模な状態管理。外部ライブラリ不要。
Zustand:シンプルで直感的なストア
Zustandは、Reduxの複雑さを解消したシンプルなグローバル状態管理ライブラリです。ボイラープレートが少なく、学習コストが低いのが特徴です。
基本的な使い方
import { create } from "zustand";
// ストアの型定義
interface AuthStore {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
updateProfile: (updates: Partial<User>) => void;
}
// ストアの作成
const useAuthStore = create<AuthStore>((set) => ({
user: null,
isAuthenticated: false,
login: (user) =>
set({ user, isAuthenticated: true }),
logout: () =>
set({ user: null, isAuthenticated: false }),
updateProfile: (updates) =>
set((state) => ({
user: state.user ? { ...state.user, ...updates } : null,
})),
}));
// コンポーネントでの使用
function UserMenu() {
// 必要な値だけセレクタで取得(不要な再レンダリングを防止)
const user = useAuthStore((state) => state.user);
const logout = useAuthStore((state) => state.logout);
if (!user) return <button>ログイン</button>;
return (
<div>
<span>{user.name}</span>
<button onClick={logout}>ログアウト</button>
</div>
);
}
ミドルウェアの活用
import { create } from "zustand";
import { devtools, persist, immer } from "zustand/middleware";
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit<CartItem, "quantity">) => void;
removeItem: (id: number) => void;
updateQuantity: (id: number, quantity: number) => void;
clearCart: () => void;
totalPrice: () => number;
}
const useCartStore = create<CartStore>()(
devtools(
persist(
immer((set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
existing.quantity += 1; // immerにより直接変更可能
} else {
state.items.push({ ...item, quantity: 1 });
}
}),
removeItem: (id) =>
set((state) => {
state.items = state.items.filter((i) => i.id !== id);
}),
updateQuantity: (id, quantity) =>
set((state) => {
const item = state.items.find((i) => i.id === id);
if (item) item.quantity = Math.max(0, quantity);
}),
clearCart: () => set({ items: [] }),
totalPrice: () =>
get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
})),
{ name: "cart-storage" } // localStorageに永続化
),
{ name: "CartStore" } // DevToolsでの表示名
)
);
非同期処理の扱い
interface ProductStore {
products: Product[];
isLoading: boolean;
error: string | null;
fetchProducts: () => Promise<void>;
}
const useProductStore = create<ProductStore>((set) => ({
products: [],
isLoading: false,
error: null,
fetchProducts: async () => {
set({ isLoading: true, error: null });
try {
const res = await fetch("/api/products");
const data = await res.json();
set({ products: data, isLoading: false });
} catch (error) {
set({ error: "商品の取得に失敗しました", isLoading: false });
}
},
}));
Jotai:アトミックな状態管理
Jotaiは、Recoilに着想を得たアトミックな状態管理ライブラリです。状態を小さな「アトム」の単位で管理し、必要なアトムだけを購読するコンポーネントのみが再レンダリングされます。
基本的な使い方
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
// プリミティブアトム
const countAtom = atom(0);
const nameAtom = atom("");
const isDarkModeAtom = atom(false);
// 派生アトム(読み取り専用)
const doubleCountAtom = atom((get) => get(countAtom) * 2);
// 派生アトム(読み書き可能)
const uppercaseNameAtom = atom(
(get) => get(nameAtom).toUpperCase(),
(get, set, newValue: string) => {
set(nameAtom, newValue);
}
);
// コンポーネントでの使用
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubleCount = useAtomValue(doubleCountAtom); // 読み取り専用
return (
<div>
<p>カウント: {count} (2倍: {doubleCount})</p>
<button onClick={() => setCount((c) => c + 1)}>増加</button>
</div>
);
}
複雑な状態の管理
import { atom, useAtom, useAtomValue } from "jotai";
import { atomWithStorage } from "jotai/utils";
// localStorageと同期するアトム
const themeAtom = atomWithStorage<"light" | "dark">("theme", "light");
// Todo管理のアトム設計
interface Todo {
id: number;
text: string;
completed: boolean;
}
const todosAtom = atom<Todo[]>([]);
// フィルター状態
const filterAtom = atom<"all" | "active" | "completed">("all");
// フィルタリング結果(派生アトム)
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
switch (filter) {
case "active":
return todos.filter((t) => !t.completed);
case "completed":
return todos.filter((t) => t.completed);
default:
return todos;
}
});
// 統計アトム
const todoStatsAtom = atom((get) => {
const todos = get(todosAtom);
return {
total: todos.length,
completed: todos.filter((t) => t.completed).length,
active: todos.filter((t) => !t.completed).length,
};
});
// アクション用のwrite-onlyアトム
const addTodoAtom = atom(null, (get, set, text: string) => {
set(todosAtom, (prev) => [
...prev,
{ id: Date.now(), text, completed: false },
]);
});
const toggleTodoAtom = atom(null, (get, set, id: number) => {
set(todosAtom, (prev) =>
prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
);
});
// コンポーネント
function TodoApp() {
const filteredTodos = useAtomValue(filteredTodosAtom);
const stats = useAtomValue(todoStatsAtom);
const [filter, setFilter] = useAtom(filterAtom);
const addTodo = useSetAtom(addTodoAtom);
const toggleTodo = useSetAtom(toggleTodoAtom);
return (
<div>
<p>全{stats.total}件 / 完了{stats.completed}件 / 未完了{stats.active}件</p>
<div>
{(["all", "active", "completed"] as const).map((f) => (
<button key={f} onClick={() => setFilter(f)} disabled={filter === f}>
{f}
</button>
))}
</div>
{filteredTodos.map((todo) => (
<div key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.completed ? "✓" : "○"} {todo.text}
</div>
))}
</div>
);
}
TanStack Query:サーバー状態管理の決定版
TanStack Query(旧React Query)は、サーバーから取得するデータの管理に特化したライブラリです。キャッシュ、バックグラウンド再取得、楽観的更新などを自動で処理します。
基本的な使い方
import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } from "@tanstack/react-query";
// QueryClientの設定
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分間はキャッシュを新鮮とみなす
retry: 2,
refetchOnWindowFocus: true,
},
},
});
// App ラッパー
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
}
// データ取得関数
async function fetchUsers(params?: { page?: number; role?: string }): Promise<{
users: User[];
total: number;
}> {
const query = new URLSearchParams();
if (params?.page) query.set("page", String(params.page));
if (params?.role) query.set("role", params.role);
const res = await fetch(`/api/users?${query}`);
if (!res.ok) throw new Error("ユーザーの取得に失敗しました");
return res.json();
}
// useQueryによるデータ取得
function UserList() {
const [page, setPage] = useState(1);
const { data, isLoading, error, isFetching } = useQuery({
queryKey: ["users", { page }],
queryFn: () => fetchUsers({ page }),
});
if (isLoading) return <p>読み込み中...</p>;
if (error) return <p>エラー: {error.message}</p>;
return (
<div>
{isFetching && <span>更新中...</span>}
{data?.users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
<button onClick={() => setPage((p) => p + 1)}>次のページ</button>
</div>
);
}
useMutationによるデータ更新
function CreateUserForm() {
const queryClient = useQueryClient();
const createMutation = useMutation({
mutationFn: async (newUser: UserCreateInput) => {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newUser),
});
if (!res.ok) throw new Error("ユーザーの作成に失敗しました");
return res.json();
},
// 成功時にユーザー一覧のキャッシュを無効化
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
const handleSubmit = (formData: UserCreateInput) => {
createMutation.mutate(formData);
};
return (
<form onSubmit={/* ... */}>
{createMutation.isPending && <p>作成中...</p>}
{createMutation.isError && <p>エラー: {createMutation.error.message}</p>}
{createMutation.isSuccess && <p>作成しました</p>}
{/* フォームフィールド */}
</form>
);
}
楽観的更新
const toggleTodoMutation = useMutation({
mutationFn: async ({ id, completed }: { id: number; completed: boolean }) => {
const res = await fetch(`/api/todos/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ completed }),
});
return res.json();
},
// 楽観的更新:APIレスポンスを待たずにUIを更新
onMutate: async ({ id, completed }) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previousTodos = queryClient.getQueryData<Todo[]>(["todos"]);
queryClient.setQueryData<Todo[]>(["todos"], (old) =>
old?.map((t) => (t.id === id ? { ...t, completed } : t))
);
return { previousTodos };
},
// エラー時にロールバック
onError: (err, variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(["todos"], context.previousTodos);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
ライブラリの使い分け指針
選定フローチャート
サーバーから取得するデータの管理が主な目的なら → TanStack Query。キャッシュ、再取得、楽観的更新が自動で処理されるため、自前で実装する必要がありません。
グローバルなクライアント状態が必要なら → Zustand。認証情報、UI設定、カート情報など、複数のコンポーネントで共有する状態に最適です。
きめ細かいレンダリング最適化が必要なら → Jotai。アトム単位の購読により、変更されたアトムに関連するコンポーネントだけが再レンダリングされます。
小規模な状態管理なら → useState/useContext。外部ライブラリが不要で、最もシンプルです。
推奨する組み合わせパターン
// 最もよくある組み合わせ
// TanStack Query(サーバー状態) + Zustand(クライアント状態)
// Zustand: UIの状態管理
const useUIStore = create<UIStore>((set) => ({
sidebarOpen: false,
theme: "light",
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setTheme: (theme: string) => set({ theme }),
}));
// TanStack Query: サーバーデータの管理
function Dashboard() {
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
const { data: stats } = useQuery({
queryKey: ["dashboard-stats"],
queryFn: fetchDashboardStats,
refetchInterval: 30000, // 30秒ごとに自動更新
});
const { data: notifications } = useQuery({
queryKey: ["notifications"],
queryFn: fetchNotifications,
});
return (
<div>
{sidebarOpen && <Sidebar />}
<main>
<StatsCards stats={stats} />
<NotificationList notifications={notifications} />
</main>
</div>
);
}
まとめ
2026年のReact状態管理は、「状態の種類に応じたツールの使い分け」が基本方針です。
Zustandは、シンプルなAPIでグローバル状態を管理できる万能ツールです。Reduxから移行する場合の第一候補として適しています。ミドルウェアによる永続化やDevTools連携も充実しています。
Jotaiは、アトミックな状態管理でRecoil的なアプローチを好む場合に適しています。派生アトムによるきめ細かいレンダリング最適化が強みです。
TanStack Queryは、サーバー状態の管理に特化した決定版です。キャッシュ、バックグラウンド再取得、楽観的更新などを自動化し、データ取得まわりのコードを大幅に削減できます。
多くのプロジェクトでは、TanStack Query(サーバー状態)+ Zustand(クライアント状態)の組み合わせがバランスの良い選択となります。プロジェクトの規模と要件に応じて、最適な組み合わせを選んでください。
関連記事
AIエージェント開発入門|自律型AIの仕組みと構築方法を解説【2026年版】
AI駆動コーディングワークフロー|Claude Code・Cursor・Copilotの実践的使い分け
プロンプトエンジニアリング上級編|Chain-of-Thought・Few-Shot・ReActの実践
APIレート制限の設計と実装|トークンバケット・スライディングウィンドウ解説
APIバージョニング戦略|URL・ヘッダー・クエリパラメータの使い分け
BIツール入門|Metabase・Redash・Looker Studioでデータ可視化する方法
チャットボット開発入門|LINE Bot・Slack Botの構築方法と活用事例
CI/CDパイプラインの基礎|継続的インテグレーション・デリバリーの全体像