React状態管理2026 - Zustand, Jotai, Redux徹底比較


はじめに

React状態管理は、2026年現在選択肢が多様化しています。

「どれを選べばいいのか?」という疑問に答えるため、主要ライブラリの特徴・使い分けを徹底解説します。

状態管理の2つの種類

  1. クライアント状態 (Client State)

    • UIの状態(モーダル開閉、フォーム入力)
    • アプリケーション内で完結
  2. サーバー状態 (Server State)

    • APIから取得したデータ
    • キャッシュ・同期が必要

2026年の主要ライブラリ

クライアント状態:

  • useState / useReducer / Context(ビルトイン)
  • Zustand(軽量・シンプル)
  • Jotai(アトミック)
  • Redux Toolkit(大規模向け)

サーバー状態:

  • TanStack Query(旧React Query)
  • SWR
  • Apollo Client(GraphQL)

ビルトイン(useState/useContext)

useState - 基本

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

useReducer - 複雑な状態

import { useReducer } from 'react';

type State = {
  count: number;
  step: number;
};

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setStep'; payload: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <input
        type="number"
        value={state.step}
        onChange={(e) => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
      />
    </div>
  );
}

Context - グローバル状態

import { createContext, useContext, useState, ReactNode } from 'react';

type Theme = 'light' | 'dark';

const ThemeContext = createContext<{
  theme: Theme;
  toggleTheme: () => void;
} | null>(null);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');

  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
}

// 使用例
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}

function Header() {
  const { theme, toggleTheme } = useTheme();
  return (
    <header className={theme}>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </header>
  );
}

Contextの課題

  • パフォーマンス: 値が変わると全コンポーネント再レンダリング
  • スケールしない: 複数Contextをネストすると可読性低下

Zustand - シンプル・軽量

インストール

npm install zustand

基本的な使い方

import { create } from 'zustand';

interface CounterStore {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

function Counter() {
  const { count, increment, decrement, reset } = useCounterStore();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

セレクター(部分購読)

// 必要な値だけ購読 → 不要な再レンダリング防止
function CountDisplay() {
  const count = useCounterStore((state) => state.count);
  return <p>Count: {count}</p>;
}

function IncrementButton() {
  const increment = useCounterStore((state) => state.increment);
  return <button onClick={increment}>+</button>;
}

ミドルウェア(persist)

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useUserStore = create(
  persist<UserStore>(
    (set) => ({
      user: null,
      login: (user) => set({ user }),
      logout: () => set({ user: null }),
    }),
    {
      name: 'user-storage', // localStorageのキー
    }
  )
);

immer(イミュータブル更新を簡単に)

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

interface TodoStore {
  todos: { id: number; text: string; done: boolean }[];
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
}

const useTodoStore = create<TodoStore>()(
  immer((set) => ({
    todos: [],
    addTodo: (text) =>
      set((state) => {
        state.todos.push({ id: Date.now(), text, done: false });
      }),
    toggleTodo: (id) =>
      set((state) => {
        const todo = state.todos.find((t) => t.id === id);
        if (todo) todo.done = !todo.done;
      }),
  }))
);

複数ストアの分割

// stores/auth.ts
export const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
}));

// stores/cart.ts
export const useCartStore = create<CartStore>((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) => set((state) => ({
    items: state.items.filter((item) => item.id !== id),
  })),
}));

// コンポーネントで使用
function Header() {
  const user = useAuthStore((state) => state.user);
  const itemCount = useCartStore((state) => state.items.length);

  return (
    <header>
      {user && <p>Welcome, {user.name}</p>}
      <p>Cart: {itemCount} items</p>
    </header>
  );
}

Jotai - アトミック状態管理

インストール

npm install jotai

Atom(状態の最小単位)

import { atom, useAtom } from 'jotai';

// プリミティブAtom
const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

派生Atom

import { atom, useAtom, useAtomValue } from 'jotai';

const firstNameAtom = atom('John');
const lastNameAtom = atom('Doe');

// 読み取り専用の派生Atom
const fullNameAtom = atom((get) => {
  return `${get(firstNameAtom)} ${get(lastNameAtom)}`;
});

function FullName() {
  const fullName = useAtomValue(fullNameAtom);
  return <p>{fullName}</p>;
}

function NameForm() {
  const [firstName, setFirstName] = useAtom(firstNameAtom);
  const [lastName, setLastName] = useAtom(lastNameAtom);

  return (
    <div>
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
    </div>
  );
}

書き込み可能な派生Atom

const priceAtom = atom(100);
const taxRateAtom = atom(0.1);

const priceWithTaxAtom = atom(
  (get) => get(priceAtom) * (1 + get(taxRateAtom)), // 読み取り
  (get, set, newPrice: number) => {
    // 税込価格から本体価格を逆算
    set(priceAtom, newPrice / (1 + get(taxRateAtom)));
  }
);

非同期Atom

const userIdAtom = atom(1);

const userAtom = atom(async (get) => {
  const userId = get(userIdAtom);
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
});

function UserProfile() {
  const user = useAtomValue(userAtom); // Suspenseで待機
  return <p>{user.name}</p>;
}

// Suspenseでラップ
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
    </Suspense>
  );
}

atomWithStorage(永続化)

import { atomWithStorage } from 'jotai/utils';

const darkModeAtom = atomWithStorage('darkMode', false);

function ThemeToggle() {
  const [darkMode, setDarkMode] = useAtom(darkModeAtom);

  return (
    <button onClick={() => setDarkMode(!darkMode)}>
      {darkMode ? '🌙' : '☀️'}
    </button>
  );
}

Redux Toolkit - 大規模アプリ向け

インストール

npm install @reduxjs/toolkit react-redux

スライス定義

// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
  step: number;
}

const initialState: CounterState = {
  value: 0,
  step: 1,
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += state.step;
    },
    decrement: (state) => {
      state.value -= state.step;
    },
    setStep: (state, action: PayloadAction<number>) => {
      state.step = action.payload;
    },
    reset: (state) => {
      state.value = 0;
    },
  },
});

export const { increment, decrement, setStep, reset } = counterSlice.actions;
export default counterSlice.reducer;

ストア設定

// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Hooks

// app/hooks.ts
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

コンポーネントで使用

import { useAppDispatch, useAppSelector } from './app/hooks';
import { increment, decrement, reset } from './features/counter/counterSlice';

function Counter() {
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
    </div>
  );
}

非同期処理(createAsyncThunk)

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

export const fetchUser = createAsyncThunk(
  'user/fetchUser',
  async (userId: string) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

TanStack Query - サーバー状態管理

インストール

npm install @tanstack/react-query

セットアップ

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <UserList />
    </QueryClientProvider>
  );
}

useQuery - データ取得

import { useQuery } from '@tanstack/react-query';

function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('/api/users');
      return response.json();
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

useMutation - データ更新

import { useMutation, useQueryClient } from '@tanstack/react-query';

function CreateUser() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newUser: { name: string; email: string }) => {
      const response = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      });
      return response.json();
    },
    onSuccess: () => {
      // キャッシュ無効化 → 再取得
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  return (
    <button
      onClick={() => mutation.mutate({ name: 'Alice', email: 'alice@example.com' })}
    >
      Create User
    </button>
  );
}

選定フローチャート

状態の種類は?
├─ サーバー状態(API取得データ)
│  └─ TanStack Query / SWR

└─ クライアント状態
   ├─ 単一コンポーネント
   │  └─ useState / useReducer

   ├─ 親子間(浅い階層)
   │  └─ Props / Context

   └─ グローバル(アプリ全体)
      ├─ シンプル・小〜中規模
      │  └─ Zustand

      ├─ 細かい最適化が必要
      │  └─ Jotai

      └─ 大規模・複雑なビジネスロジック
         └─ Redux Toolkit

まとめ

ライブラリ比較表

ライブラリサイズ学習曲線適用範囲おすすめ度
useState/Context0KB小規模⭐⭐⭐
Zustand1.2KB小〜大⭐⭐⭐⭐⭐
Jotai3KB小〜中⭐⭐⭐⭐
Redux Toolkit12KB大規模⭐⭐⭐
TanStack Query13KBサーバー状態⭐⭐⭐⭐⭐

2026年のベストプラクティス

  • クライアント状態: Zustand(シンプル・軽量)
  • サーバー状態: TanStack Query(キャッシュ・同期)
  • 大規模: Redux Toolkit(チーム開発・標準化)

次のステップ

状態管理ライブラリを適切に選び、保守性の高いReactアプリを構築しましょう。