Michi's Tech Blog

一人前のWebエンジニアを目指して

Reactキャッチアップへの道④ 『グローバルステートの管理』

こんにちは! スマレジ テックファームのMichiです!

前週に引き続き、Reactのキャッチアップをしていきます。今回は『グローバルステートの管理』について学びます。

グローバルステートの管理方法

Reactにおけるグローバルステートの管理方法は2つあります。

  1. Reactの標準機能であるContextを使う
  2. Reduxなどの外部ライブラリを使用する

本記事では、順番に両方とも解説していきます。

Contextを使用する方法

Reactには標準で『Context APIというコンポーネントツリー全体でデータを共有するための機能を提供しています。ここでは、簡単なカウンター機能を実装し、そのカウンター値をContextを使ってグローバル管理できるようにしてみます。

まずは、Contextを提供するためのコンポーネントを作成します。srcディレクトリ配下にprovidersという名称でディレクトリを作成し、その配下にCounterProvider.jsxコンポーネントを作成します。

providers/CounterProvider.jsx

import { createContext, useState } from "react";

// コンテキストの作成
export const CounterContext = createContext();

// コンテキストプロバイダーの作成
export const CounterProvider = (props) => {
  const { children } = props;
  const [count, setCount] = useState(0);

  // カウンターの値を更新する関数
  const increase = () => {
    setCount(count + 1);
  };

  // コンテキスト値と関数を提供
  return (
    <CounterContext.Provider value={{ count, increase }}>
      {children}
    </CounterContext.Provider>
  );
};

createContext関数を使って、CounterContextという新しいコンテキストを作成します。次に、CounterProviderという関数コンポーネントを定義し、カウンター処理の中身を記述します。

CounterProviderpropschildrenを受け取ります。また、rerurnしている<CounterContext.Provider>value には、グローバルで管理したい値や関数をオブジェクトの形式で渡します。こうすることで、囲っている{children}の要素の中で、Contextの値が使用できるようになります。


次に、カウンターを使用するコンポーネントを作成しましょう。

Counter.jsx

import { useContext } from "react";

import { CounterContext } from "./providers/CounterProvider";

export const Counter = () => {
  // コンテキストの値を取得
  const { count, increment } = useContext(CounterContext);

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

他のコンポーネントからContextの値を使用するには、ReactのhooksであるuseContextを使います。

この例では、useContextに先ほど作成したCounterContextを引数として渡すことで、countincreaseを取得しています。Contextから取得したcountを表示し、increase関数を<button>のonClickイベントにバインドします。


最後に、アプリケーション全体でコンテキストを利用できるように設定します。

App.js

import { Counter } from "./Counter";
import { CounterProvider } from "./providers/CounterProvider";

export default function App() {
  return (
    //  コンテキストを使用したい要素をプロバイダーで囲む
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
}

ルートコンポーネントであるApp.js内で、CounterコンポーネントCounterProviderでラップしています。これにより、Counter.jsx内でも、カウンター値をグローバルなステートとして扱えるようになりました。

外部ライブラリを使用する方法(Redux)

次に、外部ライブラリを使用する方法です。今回はReactの代表的な状態管理ライブラリである『Redux』を使用します。

まず最初に、Reduxを使用するにあたって必要な、『State, Reducer, Actions』の概念について簡単に解説します。

Reduxの基本概念:

ステート(State)

ステートはその名の通り、グローバルで状態管理される値のことを指します。このグローバルな状態は、Reduxのストアと呼ばれるコンテナに格納されます。

初期状態のステートは次のような形式のオブジェクトで定義されます。

// 初期状態の定義
const initialState = {
  count: 0
};

リデューサー(Reducer)

リデューサーは、ストア内のステートを更新するための関数です。リデューサーは、現在のステートとアクションを受け取り、新しいステートを返す役割を果たします。ステートの変更は、常にこのリデューサーを通してのみ行います。

リデューサーは次のような形式の関数で定義されます。第一引数にはステート(デフォルトには初期状態の値)、第二引数にはアクションを取ります。

const reducer = (state = initialState, action) => {
  // 状態の変更を定義するロジックを書く
  return newState;
};

アクション(Actions)

アクションは、アプリケーション内で何が起こったかを示す情報のオブジェクトです。例えば、ボタンがクリックされた、データが取得されたなど、何らかのイベントや操作を表現します。アクションは、リデューサーに特定の処理を実行するよう指示するために使用されます。

アクションは以下のような形式のオブジェクトで表現されます:

const action = {
  type: 'ACTION_TYPE', 
  payload: data // 必要に応じて追加のデータを含めることができる
};

実践

準備

では、実践編です。Contextの時と同じく、簡単なカウンター機能を実装します。

まず、Reduxのインストールとセットアップを行います。

npm install redux react-redux


次に、srcディレクトリ配下にstoreという名称でディレクトリを作成し、その配下にindex.jsファイルを作成します。

store/index.js

import { createStore } from "redux";

// 初期状態の定義
const initialState = {
  count: 0
};

// アクションタイプの定義
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

// リデューサーの定義
const reducer = (state = initialState, action) => {
  switch (action.type) {
    // カウントアップ時の挙動
    case INCREMENT:
      return {
        count: state.count + 1
      };
    // カウントダウン時の挙動
    case DECREMENT:
      return {
        count: state.count - 1
      };
    // デフォルトでは、ストアのステート値をそのまま返す
    default:
      return state;
  }
};

// createStoreにリデューサーをセットして、ストアを作成する
const store = createStore(reducer);

export default store;


Contextを使用するときと同様に、Reduxの場合も、ストアを利用したい要素をReduxから提供されているProviderで囲む必要があります。<Provider>storeには、先ほどindex.jsで定義したstoreを渡します。

App.js

import { Provider } from "react-redux";

import { Counter } from "./Counter";
import store from "./store/index";

export default function App() {
  return (
    //  ストアを使用したい要素をプロバイダーで囲む
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

ステートの利用

それでは、ストアのステートの値をCounter.jsxコンポーネントに表示させてみましょう。

ステートの値を取得するには、useSelectorというhooksを使用します。

Counter.jsx

import { useSelector } from "react-redux";

export const Counter = () => {
  // ステートの値を取得
  const count = useSelector((state) => state.count);

  return (
    <div>
      <p>Count: {count}</p>
    </div>
  );
};

ステートの更新

次は、ストアのステートの値をCounter.jsxから更新します。

ステートの値を更新するには、最初に述べた通りリデューサーを通して行う必要があります。このリデューサーを呼び出すための関数がdispatchです。

dispatchuseDispatchというhooksから取得できます。取得したdispatchにアクションタイプを伝えて、リデューサーの中のどの更新処理を呼び出すかを決定します。

import { useDispatch, useSelector } from "react-redux";

// アクションタイプをインポート
import { INCREMENT, DECREMENT } from "./store/index";

export const Counter = () => {
  const count = useSelector((state) => state.count);
  // 関数dispatchを取得
  const dispatch = useDispatch();

  // カウント処理関数の定義
  // アクションタイプをdispatchに伝えて、リデューサーを呼び出す
  const increase = () => {
    dispatch({ type: INCREMENT });
  };
  const decrease = () => {
    dispatch({ type: DECREMENT });
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increase}>Increment</button>
      <button onClick={decrease}>Decrement</button>
    </div>
  );
};

これで、Reduxのグローバルステート管理を利用した、カウンター機能を実装することができました。

どうやって使い分けるのか?

さて、ここまで学んだところで一つ思うところがあります。

「ContextとRedux、どうやって使い分けるねん?」と。

ContextとReduxのメリットを簡単にまとめてみました。

Contextのメリット

  • 小規模なアプリケーションや単純なコンポーネント間のデータ共有に適している
  • グローバルステートの管理が比較的簡単で、追加のライブラリやセットアップが不要
  • React自体に組み込まれている機能のため、学習コストが低い

Reduxのメリット

  • 複雑なアプリケーションや大規模な状態管理が必要な場合に適している
  • リデューサーやアクションを使用した高度な状態管理(更新)機能が使える
  • ミドルウェアを使用して非同期操作やサイドエフェクトを処理することができる


まとめると、Contextはシンプルさがメリットで、小規模なアプリケーションや特定の小さい範囲での状態管理に適しているようです。

反対に、Redux(を含む外部の状態管理ライブラリ)は複雑で大規模なアプリケーションでの状態管理に向いており、ユーザー情報やアプリのマスタ情報などのアプリケーション全体で管理したい値に使用される傾向があります。

このあたりは、自分も実務でReactを触ったことがないので、想像しにくい場面ではあります。

まとめ

Reactキャッチアップの4週目は、ContextとReduxを使ったグローバルステートの管理方法についてでした。

さて、5月も最終週ですので、Reactの記事はいったんここで終わりにしたいと思います。来月からはTypeScriptのキャッチアップ記事を投稿する予定になります。

来月もまたよろしくお願いします。