Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

ReactのuseEffect依存配列とイミュータビリティ完全ガイド:よくあるバグと解決策

記事のサムネイル
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

ReactのuseEffect依存配列とイミュータビリティ完全ガイド:よくあるバグと解決策

Reactの世界では、useEffectフックと状態管理はアプリケーション開発の中心的な部分です。しかし、多くの開発者が依存配列の扱い方やイミュータビリティに関する問題に頭を悩ませています。「なぜ私のエフェクトが無限ループを引き起こすのか?」「なぜ状態が更新されないのか?」このようなよくある質問に対する答えを、この記事では具体的なコード例と共に解説していきます。

「コードの美しさは、その複雑さではなく、シンプルさによって測られる」—Grady Booch

useEffect依存配列のよくある問題と正しい使い方

useEffectフックは、Reactコンポーネントの副作用を扱うための強力なツールですが、その依存配列の使い方を誤ると、予期しない動作やパフォーマンスの問題を引き起こす可能性があります。

問題1: 依存配列を指定しない

// 問題のあるコード
useEffect(() => {
  // 何らかの処理
  console.log('このエフェクトは毎回のレンダリングで実行されます');
}); // 依存配列がない

依存配列を指定しない場合、このエフェクトはコンポーネントが再レンダリングされるたびに実行されます。これは、多くの場合、必要以上の処理を引き起こし、パフォーマンスの低下につながります。

問題2: 必要な依存関係を含めていない

// 問題のあるコード
const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `カウント: ${count}`;
}, []); // countが依存配列にない

このコードでは、countの値が変更されても、ドキュメントのタイトルは更新されません。ESLintのreact-hooks/exhaustive-depsルールは、このような問題を検出するのに役立ちます。

正しい使い方: 適切な依存配列を指定する

// 正しいコード
const [count, setCount] = useState(0);

useEffect(() => {
  document.title = `カウント: ${count}`;
}, [count]); // countが依存配列に含まれている

依存配列にcountを含めることで、countの値が変更されるたびにエフェクトが再実行され、ドキュメントのタイトルが適切に更新されます。

エフェクトの実行タイミングを理解する

依存配列によって、エフェクトの実行タイミングは以下のように制御されます:

  1. 依存配列なし (useEffect(() => {}, )): コンポーネントが再レンダリングされるたびに実行
  2. 空の依存配列 (useEffect(() => {}, [])): コンポーネントの初回マウント時のみ実行
  3. 依存値を含む配列 (useEffect(() => {}, [a, b])): 初回マウント時と、いずれかの依存値が変更されたときに実行

適切な依存配列を選択することで、必要なときにのみエフェクトを実行し、不要な再レンダリングを防ぐことができます。

空の依存配列とクリーンアップ関数の正しい実装

空の依存配列 [] を使用したエフェクトは、コンポーネントのマウント時に一度だけ実行されるため、ページロード時のデータ取得やイベントリスナーの設定などに便利です。しかし、特にクリーンアップ関数と組み合わせる場合には、いくつかの落とし穴があります。

クリーンアップ関数が必要なケース

クリーンアップ関数は、以下のような状況で特に重要です:

  • イベントリスナーの削除
  • タイマーやインターバルのクリア
  • WebSocketやAPIサブスクリプションの解除
// 問題のあるコード: propsを使用するクリーンアップ関数
function Modal(props) {
  useEffect(() => {
    // 何らかの処理
    return () => {
      // クリーンアップ処理
      props.onClose(); // ESLintエラー: 依存配列にpropsが必要
    };
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
}

上記のコードでは、ESLintのルールを無効にしていますが、これは実際には問題を隠しているだけです。

正しい実装方法

解決策は、依存配列に必要な値を含めるか、useCallbackを使用して関数を安定化させることです。

// 正しいアプローチ1: 依存配列に含める
function Modal(props) {
  useEffect(() => {
    // 何らかの処理
    return () => {
      props.onClose();
    };
  }, [props.onClose]); // onCloseを依存配列に含める
}

// 正しいアプローチ2: 分割代入を使用する
function Modal({ onClose }) {
  useEffect(() => {
    // 何らかの処理
    return () => {
      onClose();
    };
  }, [onClose]);
}

React.useEffectEvent(React 19+)を使用した解決策

React 19では、useEffectEventというフックが導入され、エフェクトの依存関係から値を分離できるようになります。

// React 19の機能(2025年5月現在、まだ実験的な機能です)
function Modal(props) {
  const onCloseEvent = useEffectEvent(() => {
    props.onClose();
  });

  useEffect(() => {
    // 何らかの処理
    return () => {
      onCloseEvent();
    };
  }, []); // 依存配列は空でも問題なし
}

useEffectEventを使用すると、クリーンアップ関数内で最新のpropsを参照できるため、空の依存配列でも安全に使用できます。

タイマーとイベントリスナーの適切なクリーンアップ

function TimerComponent() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('定期的な処理');
    }, 1000);
    
    // クリーンアップ関数でタイマーをクリア
    return () => {
      clearInterval(timer);
    };
  }, []); // コンポーネントのマウント時のみタイマーを設定
  
  // ...
}

このように、クリーンアップ関数を適切に実装することで、メモリリークやその他の問題を防ぎ、コンポーネントがクリーンにアンマウントされるようにできます。

オブジェクトと配列を依存配列に含める際の注意点

オブジェクトや配列を依存配列に含める場合、意図しない再レンダリングや無限ループの原因となることがあります。これは、JavaScriptでは、オブジェクトと配列が参照型であり、内容が同じでも新しいインスタンスは異なる参照として扱われるためです。

問題: 毎回新しいオブジェクトや配列を作成するコード

function UserProfile({ userId }) {
  // 問題のあるコード:レンダリングごとに新しい配列インスタンス
  const userRoles = ['admin', 'editor'];
  
  // 問題のあるコード:レンダリングごとに新しいオブジェクトインスタンス
  const userConfig = { theme: 'dark', notifications: true };
  
  useEffect(() => {
    // ユーザー情報を取得する処理
    fetchUserData(userId, userRoles, userConfig);
  }, [userId, userRoles, userConfig]); // 毎回のレンダリングでエフェクトが実行される
  
  // ...
}

上記のコードでは、コンポーネントが再レンダリングされるたびに、userRolesuserConfigは新しいインスタンスとして作成されます。そのため、依存配列の比較では常に「変更あり」と判断され、エフェクトが不必要に実行されてしまいます。

解決策1: useMemoを使用してオブジェクトをメモ化する

function UserProfile({ userId }) {
  // 解決策:useMemoを使用して、依存値が変更されない限り同じインスタンスを保持
  const userRoles = useMemo(() => ['admin', 'editor'], []);
  
  const userConfig = useMemo(() => ({ 
    theme: 'dark', 
    notifications: true 
  }), []);
  
  useEffect(() => {
    fetchUserData(userId, userRoles, userConfig);
  }, [userId, userRoles, userConfig]); // useMemoによって安定した参照を提供
  
  // ...
}

useMemoを使用することで、依存値が変更されない限り、同じオブジェクトや配列インスタンスを保持できます。

解決策2: 依存配列に含めるのはプリミティブ値のみにする

function UserProfile({ userId }) {
  // 定数として外部で定義
  const ROLE_ADMIN = 'admin';
  const ROLE_EDITOR = 'editor';
  const THEME_DARK = 'dark';
  
  useEffect(() => {
    // エフェクト内部でオブジェクトを作成
    const userRoles = [ROLE_ADMIN, ROLE_EDITOR];
    const userConfig = { theme: THEME_DARK, notifications: true };
    
    fetchUserData(userId, userRoles, userConfig);
  }, [userId]); // 依存配列にはプリミティブ値のみを含める
  
  // ...
}

この方法では、オブジェクトや配列をエフェクト内部で作成し、依存配列にはプリミティブ値のみを含めることで、不必要な再実行を防ぎます。

オブジェクトのプロパティを依存配列に含める

function UserSettings({ user }) {
  // 問題のあるコード
  useEffect(() => {
    // user オブジェクト全体が依存配列にある
    saveUserPreferences(user);
  }, [user]); // userが新しいオブジェクトの場合、常に再実行される
  
  // 解決策
  useEffect(() => {
    // 必要なプロパティのみを使用
    saveUserPreferences({
      id: user.id,
      theme: user.theme
    });
  }, [user.id, user.theme]); // 必要なプロパティのみを依存配列に含める
  
  // ...
}

オブジェクト全体ではなく、実際に使用しているプロパティのみを依存配列に含めることで、関連のない変更でエフェクトが再実行されるのを防ぎます。

配列の長さや内容を正しく比較する

function ItemList({ items }) {
  // 問題のあるコード
  useEffect(() => {
    console.log('アイテムリストが更新されました');
  }, [items]); // 配列が新しいインスタンスの場合、常に再実行される
  
  // 解決策
  const itemsCount = items.length;
  const itemIds = useMemo(() => items.map(item => item.id).join(','), [items]);
  
  useEffect(() => {
    console.log('アイテムリストが更新されました');
  }, [itemsCount, itemIds]); // 配列の長さと内容を比較
  
  // ...
}

配列の長さや内容(例:IDのリスト)を依存値として使用することで、実際に配列の内容が変わったときのみエフェクトを再実行できます。

オブジェクトと配列の依存関係を適切に扱うことで、パフォーマンスの問題を防ぎ、より予測可能なコンポーネントの動作を実現できます。

useStateとイミュータビリティ:正しい状態更新パターン

Reactの状態管理において、イミュータビリティ(不変性)を守ることは非常に重要です。React自体が状態の変更を検出するのは、参照が変わったときのみであるため、状態オブジェクトを直接変更(ミューテート)するのではなく、新しいオブジェクトを作成する必要があります。

問題: オブジェクトや配列の状態を直接変更する

// 問題のあるコード
function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'タスク1', completed: false }
  ]);
  
  const toggleTodo = (id) => {
    // 問題:配列を直接変更している
    todos.forEach(todo => {
      if (todo.id === id) {
        todo.completed = !todo.completed; // ミュータブルな更新
      }
    });
    
    setTodos(todos); // 同じ参照を使用
  };
  
  // ...
}

このコードでは、todos配列内のオブジェクトが直接変更されています。このようなミュータブルな更新には、以下の問題があります:

  1. Reactが再レンダリングをトリガーしない場合がある(参照が変わっていないため)
  2. 開発者ツールやデバッグが困難になる
  3. 予測できない振る舞いやバグが発生する可能性がある

解決策1: スプレッド構文を使用した不変的な更新

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'タスク1', completed: false }
  ]);
  
  const toggleTodo = (id) => {
    // 解決策:新しい配列を作成する
    const newTodos = todos.map(todo => 
      todo.id === id
        ? { ...todo, completed: !todo.completed } // 新しいオブジェクトを作成
        : todo
    );
    
    setTodos(newTodos); // 新しい参照を設定
  };
  
  // ...
}

この例では、mapメソッドを使って新しい配列を作成し、変更が必要なオブジェクトのみスプレッド構文(...todo)を使って新しいオブジェクトとして作成しています。

解決策2: 関数形式のupdaterを使用する

特に、前の状態に基づいて更新する場合は、関数形式のアップデーターを使用することをお勧めします:

function Counter() {
  const [count, setCount] = useState(0);
  
  // 問題のあるコード:レースコンディションの可能性
  const increment = () => {
    setCount(count + 1);
    setCount(count + 1); // 期待値は2だが、実際は1になる
  };
  
  // 解決策:関数形式のアップデーターを使用
  const incrementCorrectly = () => {
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1); // 正しく2増加する
  };
  
  // ...
}

関数形式を使用すると、Reactは最新の状態値を関数に渡すため、複数の更新が正しく適用されます。

ネストされたオブジェクトの更新

function UserProfile() {
  const [user, setUser] = useState({
    name: '田中太郎',
    address: {
      city: '東京',
      zipCode: '100-0001'
    },
    preferences: {
      theme: 'light',
      notifications: true
    }
  });
  
  // 問題のあるコード:深いネストを直接変更
  const changeZipCode = (newZipCode) => {
    user.address.zipCode = newZipCode; // 直接変更
    setUser(user); // 同じ参照
  };
  
  // 解決策:イミュータブルな更新
  const changeZipCodeCorrectly = (newZipCode) => {
    setUser({
      ...user,
      address: {
        ...user.address,
        zipCode: newZipCode
      }
    });
  };
  
  // ...
}

深くネストされたオブジェクトを更新する場合、それぞれのレベルでスプレッド構文を使用して、変更が必要なパスのみを新しいオブジェクトで置き換えます。

複雑なケースにはuseReducerを検討する

状態ロジックが複雑になる場合は、useReducerの使用を検討してください:

// アクションタイプの定義
const ACTIONS = {
  ADD_TODO: 'add-todo',
  TOGGLE_TODO: 'toggle-todo',
  DELETE_TODO: 'delete-todo'
};

// リデューサー関数
function todoReducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_TODO:
      return [...state, { 
        id: Date.now(), 
        text: action.payload.text, 
        completed: false 
      }];
      
    case ACTIONS.TOGGLE_TODO:
      return state.map(todo => 
        todo.id === action.payload.id
          ? { ...todo, completed: !todo.completed }
          : todo
      );
      
    case ACTIONS.DELETE_TODO:
      return state.filter(todo => todo.id !== action.payload.id);
      
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  
  const addTodo = (text) => {
    dispatch({ type: ACTIONS.ADD_TODO, payload: { text } });
  };
  
  const toggleTodo = (id) => {
    dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id } });
  };
  
  const deleteTodo = (id) => {
    dispatch({ type: ACTIONS.DELETE_TODO, payload: { id } });
  };
  
  // ...
}

useReducerを使用すると、状態更新ロジックを一箇所にまとめることができ、複雑な状態管理が整理しやすくなります。

「ソフトウェア開発で最も難しいのは、状態をどのように管理するかを決めることだ」—Rich Hickey

イミュータビリティを意識した状態更新パターンを採用することで、Reactアプリケーションの予測可能性とパフォーマンスを向上させることができます。

useEffect依存配列の最適化テクニック

依存配列を最適化することで、不要な再レンダリングを防ぎ、アプリケーションのパフォーマンスを向上させることができます。以下に、よく使われる最適化テクニックを紹介します。

useCallbackでコールバック関数を安定化させる

関数を依存配列に含める場合、useCallbackフックを使用して関数の参照を安定させることができます。

function SearchComponent({ onResultsFound }) {
  const [query, setQuery] = useState('');
  
  // 問題のあるコード:インラインの関数定義
  const fetchResults = () => {
    // APIからデータを取得
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => onResultsFound(data));
  };
  
  useEffect(() => {
    fetchResults();
  }, [query, fetchResults]); // fetchResultsは毎回新しい関数になる
  
  // ...
}

上記のコードでは、コンポーネントが再レンダリングされるたびに、fetchResultsは新しい関数として定義されるため、依存配列に含めると常にエフェクトが再実行されます。

function SearchComponent({ onResultsFound }) {
  const [query, setQuery] = useState('');
  
  // 解決策:useCallbackで関数をメモ化
  const fetchResults = useCallback(() => {
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => onResultsFound(data));
  }, [query, onResultsFound]); // 依存値が変更されたときのみ、新しい関数を作成
  
  useEffect(() => {
    fetchResults();
  }, [fetchResults]); // 安定した参照
  
  // ...
}

useCallbackを使用することで、依存値が変更されないかぎり、同じ関数参照を保持できます。

useMemoを使って計算コストの高い値をメモ化する

計算コストの高い値や、オブジェクト・配列の参照を安定させるために、useMemoを使用します。

function DataGrid({ data, columns }) {
  // 問題のあるコード:毎回計算される
  const processedData = data.map(item => ({
    ...item,
    fullName: `${item.firstName} ${item.lastName}`,
    total: item.price * item.quantity
  }));
  
  useEffect(() => {
    console.log('データが更新されました', processedData.length);
  }, [processedData]); // processedDataは常に新しい配列
  
  // ...
}

計算結果をメモ化することで、不要な再計算と再レンダリングを防ぎます。

function DataGrid({ data, columns }) {
  // 解決策:useMemoで計算結果をメモ化
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      fullName: `${item.firstName} ${item.lastName}`,
      total: item.price * item.quantity
    }));
  }, [data]); // dataが変更されたときのみ再計算
  
  useEffect(() => {
    console.log('データが更新されました', processedData.length);
  }, [processedData]); // 安定した参照
  
  // ...
}

依存配列からカスタムフックを使用して依存関係を抽出する

複雑な依存関係を持つエフェクトを、カスタムフックに抽出することで、コードの可読性と再利用性を向上させることができます。

// カスタムフック
function useUserData(userId) {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        
        if (isMounted) {
          setUserData(data);
          setLoading(false);
        }
      } catch (err) {
        if (isMounted) {
          setError(err);
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    return () => {
      isMounted = false;
    };
  }, [userId]);
  
  return { userData, loading, error };
}

// 使用例
function UserProfile({ userId }) {
  const { userData, loading, error } = useUserData(userId);
  
  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラーが発生しました: {error.message}</div>;
  
  return (
    <div>
      <h2>{userData.name}</h2>
      <p>{userData.email}</p>
      {/* その他のプロファイル情報 */}
    </div>
  );
}

このように、データ取得のロジックをカスタムフックに抽出することで、複雑な依存関係の管理が容易になります。

useEffectEventとその利点(React 19+)

先述したように、React 19では新しくuseEffectEventフックが導入されています。これを使うと、エフェクトの依存配列から値を除外しつつ、常に最新の値にアクセスできます。

// React 19以前の問題
function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('message', (message) => {
      // themeの最新値にアクセスするために、依存配列にthemeを含める必要がある
      showNotification(message, theme);
    });
    connection.connect();
    
    return () => connection.disconnect();
  }, [roomId, theme]); // themeが変わるたびに再接続する
  
  // ...
}

// React 19以降の解決策
function ChatRoom({ roomId, theme }) {
  // イベントハンドラとしてのロジックを分離
  const showMessageNotification = useEffectEvent((message) => {
    showNotification(message, theme);
  });
  
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('message', (message) => {
      // 常に最新のthemeにアクセスできる
      showMessageNotification(message);
    });
    connection.connect();
    
    return () => connection.disconnect();
  }, [roomId]); // themeが変わっても再接続しない
  
  // ...
}

useEffectEventを使用すると、エフェクト自体の実行タイミングと、エフェクト内で使用される値の最新性を分離して考えることができます。

上記のように様々な最適化テクニックを適切に活用することで、不要な再レンダリングを防ぎ、パフォーマンスとコードの品質を向上させることができます。

イミュータビリティをシンプルに保つためのライブラリとツール

イミュータブル(不変)なデータ構造を扱うのは、時に煩雑で複雑になることがあります。特に、深くネストされたオブジェクトを更新する場合は、多くのスプレッド構文や条件分岐が必要になり、コードの可読性が低下する可能性があります。ここでは、Reactアプリケーションでイミュータビリティを簡単に維持するためのライブラリとツールを紹介します。

Immer: イミュータブルな状態更新を簡素化

Immerは、イミュータブルな状態更新を、まるでミュータブル(可変)操作のように直感的に書けるようにするライブラリです。

// Immerを使わない場合
function updateUser(user, newName, newAge) {
  return {
    ...user,
    name: newName,
    age: newAge,
    address: {
      ...user.address,
      city: 'Tokyo'
    }
  };
}

// Immerを使用した場合
import produce from 'immer';

function updateUser(user, newName, newAge) {
  return produce(user, draft => {
    draft.name = newName;
    draft.age = newAge;
    draft.address.city = 'Tokyo';
  });
}

Immerは、内部的に「ドラフト」と呼ばれる特殊なオブジェクトを使用します。このドラフトに対して直接変更を加えることができ、Immerが自動的にこれらの変更を新しいイミュータブルなオブジェクトに変換します。

use-immer: ReactのuseStateとuseReducerのためのImmerバインディング

use-immerは、ImmerをReactのフックと統合したものです。useStateuseReducerのImmer版として、useImmeruseImmerReducerを提供します。

import { useImmer } from 'use-immer';

function TodoApp() {
  const [todos, updateTodos] = useImmer([
    { id: 1, text: 'タスク1', completed: false }
  ]);
  
  const toggleTodo = id => {
    updateTodos(draft => {
      const todo = draft.find(todo => todo.id === id);
      if (todo) {
        todo.completed = !todo.completed; // 直感的な操作
      }
    });
  };
  
  const addTodo = text => {
    updateTodos(draft => {
      draft.push({
        id: Date.now(),
        text,
        completed: false
      });
    });
  };
  
  // ...
}

useImmerを使用すると、状態更新のロジックがより直感的になり、コードの意図がより明確になります。

Immutable.js: 完全イミュータブルなデータ構造

Immutable.jsはFacebookが開発したライブラリで、完全にイミュータブルなデータ構造を提供します。

import { Map, List } from 'immutable';
import { useState } from 'react';

function UserProfile() {
  const [user, setUser] = useState(Map({
    name: '田中太郎',
    age: 30,
    address: Map({
      city: '東京',
      zipCode: '100-0001'
    }),
    hobbies: List(['読書', '旅行'])
  }));
  
  const updateCity = newCity => {
    setUser(user.setIn(['address', 'city'], newCity));
  };
  
  const addHobby = hobby => {
    setUser(user.update('hobbies', hobbies => hobbies.push(hobby)));
  };
  
  // JSオブジェクトに変換して表示
  return (
    <div>
      <h2>{user.get('name')}</h2>
      <p>年齢: {user.get('age')}</p>
      <p>住所: {user.getIn(['address', 'city'])}</p>
      <ul>
        {user.get('hobbies').map((hobby, index) => (
          <li key={index}>{hobby}</li>
        ))}
      </ul>
    </div>
  );
}

Immutable.jsは強力な機能を提供しますが、学習コストが高く、Reactのエコシステムと完全に統合されていないため、サイズや複雑さを考慮する必要があります。

useReducerとContextAPI: 状態管理の集約

複雑な状態管理には、useReducerと Context APIの組み合わせが効果的です。これによりReduxのような集中型状態管理が実現可能になります。

// 状態とコンテキスト
const initialState = {
  todos: [],
  filter: 'all'
};

const TodoContext = createContext();

// リデューサー
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload,
          completed: false
        }]
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };
    default:
      return state;
  }
}

// プロバイダーコンポーネント
function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  return (
    <TodoContext.Provider value={{ state, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
}

// コンシューマーコンポーネント
function TodoList() {
  const { state, dispatch } = useContext(TodoContext);
  
  // フィルタリングされたTodoを取得
  const filteredTodos = useMemo(() => {
    if (state.filter === 'all') return state.todos;
    return state.todos.filter(todo =>
      state.filter === 'completed' ? todo.completed : !todo.completed
    );
  }, [state.todos, state.filter]);
  
  // ...
}

Redux Toolkit: Redux簡素化ライブラリ

大規模なアプリケーションでは、Redux Toolkitを使用することで、Reduxの定型コードを減らし、イミュータブルな状態更新を簡素化できます。Redux ToolkitはImmerを内部的に使用しています。

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

// スライス(リデューサーとアクションの組み合わせ)
const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      // Immerを内部的に使用しているため、直接変更できる
      state.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    }
  }
});

// アクションクリエイターを自動生成
export const { addTodo, toggleTodo } = todosSlice.actions;

// ストアの設定
const store = configureStore({
  reducer: {
    todos: todosSlice.reducer
  }
});

// コンポーネントでの使用
function TodoApp() {
  const todos = useSelector(state => state.todos);
  const dispatch = useDispatch();
  
  const handleAddTodo = text => {
    dispatch(addTodo(text));
  };
  
  const handleToggleTodo = id => {
    dispatch(toggleTodo(id));
  };
  
  // ...
}

「あらゆる複雑なアプリケーションは、一度に一つのシンプルなステップで構築されていく」—Mark Suster

適切なライブラリやツールを選択することで、イミュータビリティの管理がより簡単になり、コードの品質と保守性が向上します。プロジェクトの規模や要件に応じて、最適なツールを選びましょう。

おすすめの書籍

おすすめコンテンツ