Reduxの用語と使い方についてまとめました【React/React-redux/Redux Toolkit】

JavaScript

Reduxは状態管理のライブラリです。Reactに標準で実装されてるReactHooksのContextuseContextと同じようにグローバルな状態を管理します。

Reduxは独特な用語も多いし、考え方が複雑なので参考書を読んでもイマイチ理解できません、、。
まずはインストール方法と使い方について調べました。

プロジェクトにReact-reduxとRedux Toolkitをインストールする

ReduxはReactと名前が似ていますが、別のライブラリになるので別途インストールが必要です。
公式サイトのチュートリアルを参考にインストールします。

Installation | Redux
Introduction > Installation: Installation instructions for Redux and related packages

Reactのインストールから始める場合、npx create-react-app <プロジェクト名>を実行してReact環境を構築しておきます。

# Reactのプロジェクトを作成 ※「my-redux-counter」はプロジェクト名
$ npx create-react-app my-redux-counter

Reactのプロジェクトに移動して、React-reduxとRedux Toolkitをインストールします。

Quick Start | Redux Toolkit

React-reduxはReactでReduxを使うためのライブラリで、Redux Toolkitは、Reduxの実装を簡単にするライブラリです。公式でも使用することが推奨されているので、両方ともインストールします。

# プロジェクトに移動
$ cd my-redux-counter

# React-reduxとRedux Toolkitをインストール
$ npm install @reduxjs/toolkit react-redux

これでReduxを使う準備ができました!

Reduxの使い方と用語

Reduxは独自の関数と用語があります。これらを把握しておかないと理解が難しいです。
簡単ですが、独自関数の使い方と用語をまとめました。

stateとは

stateは、Reduxで管理するグローバルな値(状態)を指します。
stateの値をどのコンポーネントからでも更新できて、かつ取り出せるようにするのが最終目的です。

stateは次の図のように1つが更新されたら全てのコンポーネントが更新されるイメージです。

reducer関数

reducerstateを更新するための関数で、Reactの標準機能です。
Reduxを理解するために、reducerの挙動を理解する必要があるのでメモしておきます!

reducer(state, action) { <stateを更新する処理> }

引数(state, action)には、次の値が格納されています。

  • 第一引数に更新前の値(state)
  • 第二引数に新しい値(action)

第二引数のactionは、主に更新の条件を判定するtypeプロパティと、更新する値がセットになったオブジェクトを渡します。

引数で渡されたaction.typeの値をswitch文で判定して処理を分けることが多いです。Redux Toolkitを使うと異なった書き方になりますが、reducerの使用例をメモしておきます。

reducerの使用例
actionには 次のようなオブジェクトが渡ってくる
例) {type: “ADD”, id: 0, content: “テキスト”}

const reducer = (state, action) => {
  switch (action.type) {
    case "ADD":
      return [
        ...state,
        {
          id: action.id,
          content: action.content,
        }
      ];
    case "DELETE":
      const deleteItemList = state.filter((item) => item.id !== action.id);
      return deleteItemList;
    default:
      return state;
  }
};

上記のコードは、typeプロパティが”ADD”だったら新しいオブジェクトを追加して、”DELETE”だったら、該当のオブジェクトを削除しています。
なお、全ての条件に合致しない場合は、元の値stateをそのまま返しています。

尚、stateがオブジェクトや配列の場合、stateの値を直接更新せず、新しい値を作ってから返却したほうが良いらしいです。

今回は、[…state]でstate(オブジェクト)をコピーし、新しい値をつくっています。

  return [
  ...state, // state(オブジェクト)をコピー
  {
    id: action.id,
    content: action.content,
  }
];

actionオブジェクトとは

actionオブジェクトは、stateを更新する新しい値で、reducer関数の第二引数に渡ってきます。

更新する条件を判定するためのキーワードtypeプロパティと更新値がセットになったオブジェクトです。

{type: “KEYWORD”, payload: <更新値>}

actionオブジェクトの更新値のプロパティ名は、一般的にpayloadにします。

ActionCreatorでactionオブジェクトを作成する

ActionCreator(アクションクリエイター)とは、アクションを作成するだけの関数です。
以下は、plusのactionオブジェクトをActionCreatorで作成している例です。

import { useDispatch } from 'react-redux';

const Test = () => {
  // useDispatchを使えるようにする
  const dispatch = useDispatch();

  // ActionCreatorを作成
  const plus = (payload) => {
    return {
      type: 'plus',
      payload,
    };
  };

  // ボタンをクリックしたらDispatchする
  const numPlus = () => {
      dispatch(plus(2));
      // {type: 'plus', payload: 2}がdispachで送信される
    };

  return(
    <button onClick={numPlus}>
      reducerに送信!
    </button>
  )
}

ActionCreatorを作成しているのは、以下の関数です。
引数を使ってactionオブジェクトを返却しているだけです。

 // ActionCreatorを作成
  const plus = (payload) => {
    return {
      type: 'plus',
      payload,
    };
  };

このようにactionの作成を関数にまとめることで、actionオブジェクトのコードが統一化できます。

なお、Redux Toolkitを使うと、ActionCreatorは自動的に生成されます。

useDispatch関数

useDispatchは、reducerにactionオブジェクトを送信する為の関数です。
使用するにはインポートする必要があります。

構文は次のように、useDispatch関数を実行して、戻り値を変数に格納してから使います。
※引数には、actionオブジェクトを設定します。

const dispatch = useDispatch();
dispatch({type: “KEYWORD”, payload: <更新する値>})

似たような関数にdispatch関数があります。
こちらはReactに標準で実装されている関数ですが、Reduxでは、useDispatch関数を使います。

以下はuseDispatch関数を使ってreducerにアクションを送信する例です。

useDispatchの使用例

// useDispatchをインポート
import { useDispatch } from 'react-redux';

const Component = () => {
  // useDispatchを実行して変数に格納
  const dispatch = useDispatch();
  return (
     <>
      <button className="plus" onClick={() => dispatch({type: "KEYWORD", payload: "更新する値"})}>
        ボタンをクリックしたらactionオブジェクトをreducer関数に送信する
      </button>
    </>
  );
}

useSelector関数

useSelectorは、Reduxで管理しているグローバルな値(state)を取得する関数です。
使用するにはインポートする必要があります。

以下は、useSelector()で、stateの値を取得しています。

useSelectorの使用例

import { useSelector } from 'react-redux';

const CounterResult = () => {
  const count = useSelector((state) => state);
  return <div className="result">{count}</div>;
};

stateを取り出すだけなので、あまり難しいことはなさそうです。
Redux Toolkitでは、Storeに設定するプロパティに値が格納されます。

Providerコンポーネントとは

Providerコンポーネントは、Reduxで管理するstateの影響範囲を指定します。

設定するには、Providerコンポーネントで影響範囲にしたいコンポーネントを囲むだけです。
影響範囲は、子コンポーネントにも及びます。

全てのコンポーネントを対象にする場合、手っ取り早くAppコンポーネントをProviderコンポーネントで囲みます。

// Provicerコンポーネントをインポート
import { Provider } from 'react-redux';
// configureStore関数で作成したstoreをインポート
import { store } from './redux/store';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
      <App />
  </Provider>
);

propsのstoreには、これから記述するRedux ToolkitのconfigureStore関数で作成するStoreを設定します。

Redux Toolkitの使い方と用語

ここからはRedux Toolkitの機能について調べたことをメモします。

Redux ToolkitのStoreとSliceについて

Redux Toolkitの機能として、StoreとSliceがあります。
図で表すと、Storeの中にいくつかのSliceがあって、その中にreducerやactionの機能があるイメージです。最終的にはStateを更新します。

Storeとは

Storeはグローバルな値を管理するオブジェクトで、アプリに1つだけ存在します。stateの更新やアクセスは全てStoreを介して実行されます。

Sliceとは

Sliceは、stateをどのように更新するかを定義しているreducerと、stateを更新する新しい値actionの機能がセットになったオブジェクトです。

StoreとSliceを作成するには、次のように決まった関数がRedux Toolkitに用意されています。

Storeの作成 → configureStore()
Sliceの作成 → createSlice()

configureStore関数

configureStoreはStoreを作成する関数です。
引数には、stateの状態を管理する為の機能を定義する必要があります。
つまり、Storeには、Sliceを設定することになります。

以下は基本的な雛形です。ここでは、store変数にconfigureStoreを格納します。

redux/store.js

// configureStoreをインストール
import { configureStore } from '@reduxjs/toolkit';
// createSlice関数で作成したSliceをインポート
import countReducer from './counterSlice';

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

counterプロパティにcounterSliceという名前のSliceを設定しています。
※counterSliceは別ファイルで定義したものをインポートしています。

counterプロパティはuseSelectorで値と取り出すときに使います。

configureStoreにSliceを設定する

configureStoreの引数にはreducerプロパティを持ったオブジェクトを設定します。
このreducerプロパティに、Sliceを設定します。

言葉にするとややこしいですが、要は次のようなコードです。

configureStore({
  reducer: {
    reducer1: reducerSlice1,
    reducer2: reducerSlice2,
  },
})

複数のSliceを設定するときは、上記のようにプロパティを増やしていけばOKです。

reducerオブジェクトのプロパティには、次の項目に記載するcreateSliceで作成したreducerを設定します。

createSlice関数

createSliceはreducerとActionCreatorの機能を持ったオブジェクトを生成することができます。
createSliceで作成したオブジェクトをSliceといいます。

基本的な構文は次の通りです。

// createSliceをインポート
import { createSlice } from '@reduxjs/toolkit';

// createSliceを変数counterSliceに格納
const counterSlice = createSlice({
  name: 'counter', //スライス名
  initialState: 0, // 初期値
  // reducerを生成
  reducers: {
    // タイプ名で関数を作成
    plus(state, action) {
      state + action.payload;
    },
    minus(state, { type, action }) {
      state - action.payload;
    },
  },
});

// actionクリエイター
const { plus, minus } = counterSlice.actions;

// selectorをエクスポート
export const selectorCounter = (state) => state.counter;

// actionクリエイターとreducerをエクスポートする
export { plus, minus };
export default counterSlice.reducer;

スライス名とstateの初期値を設定するのがポイントです。※スライス名はタイプ名に使われます。

Reducerを生成する

Reducerの生成は、reducersプロパティに設定します。タイプ名で関数を作成し、実装したい処理を記述します。なお、引数には通常のreducer関数と同様、stateとactionが渡ってきます。

 // reducerを生成 
 reducers: {
    // type名で関数を作成
    plus(state, action) {
      state + action.payload;
    },
    minus(state, { type, action }) {
      state - action.payload;
    },
  },

本来、reducerは、副作用のない純粋関数の必要がありますが、Redux Toolkitを使った場合、オブジェクトや配列でもstateを直接変更することができます。returnも必要ありません。

ActionCreatorを生成する

ActionCreatorはreducersを設定したら自動的に生成されます。
使用する際は、createSliceを格納したオブジェクトのactionsプロパティから取得できます。
※以下は分割代入で取得している例です。

// ActionCreator
const { plus, minus } = counterSlice.actions;

なお、作成されたActionCreatorのタイプ名は、[スライス名/タイプ名]になります。
例){type: “counter/plus”, payload: 0 }

ReducerとActionCreatorをエクスポートする

ReducerとActionCreatorもエクスポートして、他のコンポーネントで使えるようにしておきます。

// actionクリエイターとreducerをエクスポートする
export { plus, minus };
export default counterSlice.reducer;

Redux ToolkitでのuseSelectorの使い方

stateを取得する時、useSelector()を使いますが、React Toolkitの場合、stateはStoreのプロパティ名に含まれています。

具体的には以下のcounterの部分です。

redux/store.js

import { configureStore } from '@reduxjs/toolkit';
import countReducer from './counterSlice';

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

使用する際は、次のようにuseSelector()の引数にStoreのプロパティを含めて指定します。

import { useSelector } from "react-redux";

const CounterResult = () => {
  const count = useSelector((state) => state.counter);
  return <div className="result">{count}</div>;
};
export default CounterResult;

これでも問題なく動きますが、stateを返す関数をエクスポートして使い回せばコードが短くなります。

// selectorをエクスポート
export const selectorCounter = (state) => state.counter;

stateを使用したいコンポーネントでエクスポートした関数を使用すれば、コードの使い回しができます。

import { useSelector } from "react-redux";
import { selectorCounter } from './counterSlice';

const CounterResult = () => {
  const count = useSelector(selectorCounter);
  return <div className="result">{count}</div>;
};
export default CounterResult;

まとめ

改めてReduxについてまとめましたが、やはり概念が難しいです、、。

難しいですが、決められた通りに実装すれば、簡単にグローバルな値を管理できるし、機能が分割されているので、コードの見通しが良いなぁ、、。と思いました。

これからも積極的に使っていきたいと思います!