React入門第12回~useContextとuseReducerとは?~

複雑な状態管理に向けて

OZ
OZ

先生!前回のStateの記事で、useStateを使って状態を管理する方法は分かりました。でも、大きなアプリだともっと複雑な状態管理が必要になるって聞きました!!

プロ太
プロ太

いい質問だね、OZくん!そのとおり。useStateは便利だけど、状態が多くなるとコードが煩雑になることがあります!そこで登場するのが、useContext】と【useReducer】です

useContextuseReducer?それってどんなものなんですか?

簡単に言うと、useContextデータをコンポーネント間で簡単に共有するための仕組みで、useReducer複雑な状態管理をシンプルにするためのものなんだ。それじゃあ、順番に説明していこう!

1. useContextって何?

グローバルな状態を共有する

useContextを使うと、親から子へpropsで直接データを渡す方法ではなく、コンポーネントのどこからでもデータを受け取れるようになるんです。まず、例を見てみましょう!

import { createContext, useContext } from "react";

const UserContext = createContext();

const ParentComponent = () => {
  const user = { name: "OZ", age: 18 };

  return (
    <UserContext.Provider value={user}>
      <ChildComponent />
    </UserContext.Provider>
  );
};

const ChildComponent = () => {
  const user = useContext(UserContext);
  return (
    <p>
      こんにちは!{user.name}くん!年齢は、{user.age}ですね!
    </p>
  );
};

export default ParentComponent;
コード解説
  1. createContext
    • グローバルにデータを管理するための「箱」を作成。ここではUserContextを作っています。
  2. UserContext.Provider
    • データ(valueプロパティ)を子コンポーネントに共有するための仕組み。
  3. useContext(UserContext)
    • 子コンポーネントがデータを受け取るときに使う。

これなら、親から子に毎回propsを渡さなくてもいいんですね!

2. 状態が複雑な場合に便利なuseReducer

useReducerは、複雑な状態を管理するときに便利なフックです。useStateと似てるけど、状態の更新ロジックをreducerという関数にまとめて書けるのが特徴です。

カウンターアプリの例

import { useReducer } from "react";

const initialState = { count: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    case "reset":
      return initialState;
    default:
      throw new Error("不明なアクションです");
  }
};

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  debugger;
  return (
    <div>
      <p>カウント:{state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>増やす</button>
      <button onClick={() => dispatch({ type: "decrement" })}>減らす</button>
      <button onClick={() => dispatch({ type: "reset" })}>リセット</button>
    </div>
  );
};

export default Counter;

カウンターアプリのコード解説

全体の流れ
  • このアプリはカウンターを操作するもので、ボタンを押すと数値が増減したり、リセットされたりします。
  • 状態管理にuseReducerを使い、状態(state)を更新するための「アクション」(dispatch)を呼び出します。
const initialState = { count: 0 };

これは状態の初期値です
count: 0として、最初はカウンターが0に設定されます

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      throw new Error('Unhandled action type: ' + action.type);
  }
}
  • 引数の説明
    • state:現在の状態(例:{ count: 0 }
    • action:どのように状態を更新するかを指定するオブジェクト(例:{ type: 'increment' }
  • switch文の動き
    1. action.typeincrementの場合、countを1増やした新しい状態を返します
    2. decrementの場合、countを1減らします
    3. resetの場合、initialStateに戻します
    4. 想定外のaction.typeが渡された場合はエラーを投げます(バグ防止)
const [state, dispatch] = useReducer(reducer, initialState);

useReducerは2つの引数を受け取ります。

  1. 状態を更新するためのreducer関数
  2. 状態の初期値(initialState

戻り値として、次の2つが返されます。

  1. state:現在の状態(例:{ count: 0 }
  2. dispatch:状態を更新するための関数
    (例:dispatch({ type: 'increment' })
return (
  <div>
    <p>カウント: {state.count}</p>
    <button onClick={() => dispatch({ type: 'increment' })}>増やす</button>
    <button onClick={() => dispatch({ type: 'decrement' })}>減らす</button>
    <button onClick={() => dispatch({ type: 'reset' })}>リセット</button>
  </div>
);

state.count:現在のカウント値を表示。各ボタンonClickイベントでdispatchを呼び出し、状態を更新します。

  • incrementボタン:カウントを1増やす
  • decrementボタン:カウントを1減らす
  • resetボタン:初期状態にリセットする

3. useContextuseReducerを組み合わせる

useContextuseReducerを組み合わせると、もっと便利になりますよ。例えば、大規模アプリで複数のコンポーネント間で状態を共有しながら管理する場合を考えてみましょう!!

Todoリストの例

import React, { createContext, useReducer, useContext } from 'react';

// 初期状態とreducer関数
const initialState = [];
function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.payload];
    case 'remove':
      return state.filter((_, index) => index !== action.payload);
    default:
      throw new Error('Unhandled action type: ' + action.type);
  }
}

// Contextの作成
const TodoContext = createContext();

function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <TodoContext.Provider value={{ state, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
}

function TodoList() {
  const { state, dispatch } = useContext(TodoContext);

  return (
    <div>
      <ul>
        {state.map((todo, index) => (
          <li key={index}>
            {todo} <button onClick={() => dispatch({ type: 'remove', payload: index })}>削除</button>
          </li>
        ))}
      </ul>
      <button
        onClick={() => dispatch({ type: 'add', payload: `新しいタスク ${state.length + 1}` })}
      >
        タスクを追加
      </button>
    </div>
  );
}

function App() {
  return (
    <TodoProvider>
      <TodoList />
    </TodoProvider>
  );
}

export default App;
Todoリストのコード解説
全体の流れ
  • このアプリはタスク(Todo)を管理するもので、新しいタスクを追加したり、削除したりできます。
  • 複数のコンポーネントで状態を共有するため、useReduceruseContextを組み合わせています。
const initialState = [];

タスクが最初は何もない状態を配列[]で定義しています。

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, action.payload];
    case 'remove':
      return state.filter((_, index) => index !== action.payload);
    default:
      throw new Error('Unhandled action type: ' + action.type);
  }
}

動作の説明

  1. addアクションの場合:
    • action.payload(新しいタスク)を現在の状態(配列)に追加します。
    • [...state, action.payload]はスプレッド構文を使って新しい配列を作成します。
  2. removeアクションの場合:
    • 指定されたインデックス(action.payload)を除く配列を返します。
    • filterメソッドで不要なタスクを除外します。
const TodoContext = createContext();

TodoContextを作成し、アプリ全体で状態を共有できる仕組みを用意します。

function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <TodoContext.Provider value={{ state, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
}

useReducerで管理している状態とdispatchをコンテキストに渡します。
子コンポーネントでTodoContextを使えば、これらの値を利用できます。

function TodoList() {
  const { state, dispatch } = useContext(TodoContext);

  return (
    <div>
      <ul>
        {state.map((todo, index) => (
          <li key={index}>
            {todo} <button onClick={() => dispatch({ type: 'remove', payload: index })}>削除</button>
          </li>
        ))}
      </ul>
      <button
        onClick={() => dispatch({ type: 'add', payload: `新しいタスク ${state.length + 1}` })}
      >
        タスクを追加
      </button>
    </div>
  );
}

useContextの利用

  • TodoContextからstatedispatchを取得します。

タスクの表示

  • state.mapで現在のタスクをループし、それぞれをリストアイテム(<li>)として表示します。
  • 各アイテムには削除ボタンがあり、クリックするとremoveアクションが呼び出されます。

タスクの追加

  • 追加ボタンを押すと、dispatchaddアクションを呼び出し、新しいタスクを配列に追加します。

なるほど!こうすれば、どのコンポーネントからでもTodoリストを操作できますね!

次回に向けて

先生、今日もたくさん学べました!次はReduxの話を教えてください!

もちろん!Reduxは、さらに状態管理を強力にしてくれるライブラリです。次回を楽しみにしていてくださいね!