MENU

React の状態管理パターン比較 – useState vs Context API vs Zustand

目次

React の状態管理パターン比較 – useState vs Context API vs Zustand

React での状態管理の重要性

React アプリケーション開発で状態管理は、プロジェクト成功の鍵になります。「同じ状態を複数のコンポーネントから参照したい」「深い階層を通じて状態を渡すのが大変」といった React 開発者の悩みはありませんか?

React には複数の状態管理パターンがあり、それぞれ異なるユースケースに最適化されています。今回は、useState、Context API、Zustand の3つのアプローチを比較し、React での状態管理でどのパターンを選ぶべきかをまとめておきたいと思います。

対象となる方

  • React の基本的な使い方は知っている方
  • 状態管理の方法について迷っている方
  • プロジェクト規模に応じた選択肢を知りたい方

※ このドキュメントは React 18 以上で書いていきます。

useState:最も基本的な状態管理

useState の仕組みと限界

useState は React の基本的なフックで、コンポーネント内で状態を保持します。シンプルですが、いくつかの制限があります。

import { useState } from 'react';

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

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

useState の主な問題点:

  • Props Drilling:複数の階層を通じて状態を渡す必要がある
  • 単一コンポーネント内のみ:兄弟コンポーネント間での状態共有が困難
  • 複数の状態管理:複数の useState が増えると管理が複雑になる
  • リレンダリング最適化:親の再レンダリングが子に伝播する

useState の適用場面

  • 単一コンポーネント内のみで使う状態
  • フォームの入力値、トグル、モーダルの開閉など局所的な状態
  • 小規模プロジェクト・プロトタイプ開発
  • UI の一時的な状態管理

Context API:複数コンポーネント間での状態共有

Context API の基本

Context API は、コンポーネントツリー全体で状態を共有するための React 標準機能です。Props Drilling を避けられるため、中規模プロジェクトに最適です。

import { createContext, useState } from 'react';

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

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

Context API を使用するコンポーネント:

import { useContext } from 'react';
import { ThemeContext } from './ThemeProvider';

function Header() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <header style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        Toggle Theme
      </button>
    </header>
  );
}

Context API のメリット・デメリット

メリット:

  • React 標準機能なので外部ライブラリ不要
  • Props Drilling を完全に排除できる
  • シンプルな学習曲線
  • 小〜中規模プロジェクトに十分

デメリット:

  • Context の値が変更されると、すべてのコンシューマーが再レンダリングされる
  • パフォーマンス最適化が必要になることがある
  • 大規模状態管理には向かない
  • デバッグが複雑になる可能性がある

Zustand:軽量でシンプルな状態管理

Zustand の概要と利点

Zustand は、Redux よりもシンプルで、Context API よりもパフォーマンスに優れた外部ライブラリです。バンドルサイズが小さく、学習曲線も緩いです。

import { create } from 'zustand';

const useCountStore = create((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 = useCountStore((state) => state.count);
  const increment = useCountStore((state) => state.increment);

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

Zustand の特徴

  • バンドルサイズが非常に小さい(約 2.5KB)
  • ボイラープレートコードが少ない
  • 必要な状態のみをサブスクライブするため、パフォーマンスが良い
  • Redux DevTools 対応
  • TypeScript サポートが優れている

3つのアプローチの比較表

項目 useState Context API Zustand
学習曲線 最も簡単 簡単 やや簡単
バンドルサイズ 0KB(組込) 0KB(組込) ~2.5KB
パフォーマンス 中程度 中程度 優秀
適用規模 小規模 中規模 中〜大規模
ボイラープレート 少ない 中程度 少ない
デバッグ性 優秀 中程度 優秀

選択基準:プロジェクト規模別ガイド

小規模プロジェクト(単ページアプリケーション)

useState で十分です。外部ライブラリを追加する必要はありません。複数ページの場合でも、各ページ独立の状態管理なら useState で対応できます。

中規模プロジェクト(複数ページ、複数の共有状態)

Context API が推奨されます。テーマ設定、認証情報、言語設定など、アプリケーション全体で共有する状態が少ない場合は Context API が最適です。

大規模プロジェクト(複雑な状態管理、複数チーム開発)

Zustand の使用を検討しましょう。Redux も選択肢ですが、Zustand の方がセットアップが簡単で、学習曲線が緩いです。状態が極めて複雑な場合のみ Redux を検討します。

実装例:同じ機能を各パターンで実装

ユーザー情報の状態管理

ユーザーログイン情報をアプリケーション全体で共有する例です。

Context API での実装:

const UserContext = createContext();

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  const login = async (email, password) => {
    setLoading(true);
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });
      const data = await response.json();
      setUser(data.user);
      localStorage.setItem('user', JSON.stringify(data.user));
    } finally {
      setLoading(false);
    }
  };

  const logout = () => {
    setUser(null);
    localStorage.removeItem('user');
  };

  return (
    <UserContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

Zustand での実装:

const useUserStore = create((set) => ({
  user: null,
  loading: false,

  login: async (email, password) => {
    set({ loading: true });
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });
      const data = await response.json();
      set({ user: data.user });
      localStorage.setItem('user', JSON.stringify(data.user));
    } finally {
      set({ loading: false });
    }
  },

  logout: () => {
    set({ user: null });
    localStorage.removeItem('user');
  }
}));

パフォーマンス最適化のベストプラクティス

Context API の場合

  • 状態を分割し、複数の Context に分ける
  • useMemo を使って Context の値をメモ化する
  • useCallback を使ってコールバック関数をメモ化する
  • 必要に応じて React.memo でコンポーネントをラップする

Zustand の場合

  • セレクター関数で必要な状態のみをサブスクライブ
  • 浅い比較により不要なレンダリングを自動回避
  • 状態を論理的にグループ化してストアを分割

まとめ

React での状態管理の選択は、プロジェクト規模、チーム規模、要件により異なります。

  • useState:シンプルな小規模プロジェクトに最適
  • Context API:React 標準機能で中規模プロジェクトに推奨
  • Zustand:軽量で高パフォーマンス、大規模プロジェクトに最適

各パターンのメリット・デメリットを理解し、プロジェクトの要件に応じて最適なアプローチを選びましょう。必要以上に複雑な状態管理ツールを導入する必要はありません。まずは シンプルに始めて、必要に応じてスケールアップするのが賢明です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次