useReducerの使い方についてまとめました【React】

JavaScript

stateとは、コンポーネント内で状態を保持できる変数です。Reactには、stateを管理できるフックがいくつか用意されています。

stateの管理は、主にuseStateを使っていたので、useReducerを使う機会があまりありませんでした。
ちょっとわかりにくい仕様だったので、使い方をまとめました。

useReducerとは

useReducerは、useStateと同じく値を管理できるフックです。

構文は次の通りです。

const [state, dispatch] = useReducer(reducer<stateを更新する関数>, <stateの初期値>);

stateとその初期値を指定するのはuseStateと同じです。
聞き慣れない単語、dispatchreducerが出てきました。

dispatchはactionをreducer関数に送信する

このタイトルで既に分かりにくいです、、。
最低限の動きを実装したサンプルを作りました。ボタンを押すと、dispatchで送信したactionのデータがレンダリングされます。

See the Pen react-reducer by donguri2020 (@m-ke) on CodePen.

actionはreducer関数の第二引数になる

まず、新しくactionという単語が出てきました。

actionとは、何かしらリアクションが起きた時に送信するデータのことです。stateを更新する関数であるreducer関数の第二引数になります。※ちなみに第一引数はstateです。

以下は、typeとcontentプロパティを持っているactionデータ(オブジェクト)です。

// action(オブジェクト)
const action =  { 
  type: 'ACTION_TYPE',
  content: "何かしらのデータ"
}

actionといっても何の変哲もないオブジェクトです。今回はオブジェクですが、文字列や数字も設定できます。

何かしらリアクションが起きた時に送信するので、例えばクリックイベントが起きた時にactionを送信したい!とした場合、次のようにdispatchで送信する必要があります。
お作法っぽいのでそのまま覚えます。

<button onClick={() => dispatch({ type: "ACTION_TYPE" , content: "何かしらのデータ"})}>actionデータを送る</button>

reducer関数はstateとactionを受けとってstateを更新する

dispatchは、actionをどこに送るのか、、? 送信先はreducer関数です。
reducer関数は、useReducerを初期化したときに設定します。
※ここではreducerという名前にしましたが、好きな名前に変更できます。

const [state, dispatch] = useReducer(reducer, "初期値だよ");

reducer関数は引数を2つ持ちます。stateactionです。
第一引数は、現在のstateの値で、第二引数は先ほどdispatchで送信したactionです。最初はuseReducerを定義した時に設定した初期値が入ります。

// 初回だとstateにはuseReduceで設定した初期値が入る
const reducer = (state, action) => {
  console.log(action); // actionデータが格納
  console.log(state); // 更新されたstateの値
  return <stateを更新する値>
};

最終的に、reducer関数はreturnで値を返します。このreturnで返した値でstateを更新します。
コードだと以下の箇所になります。今回は受け取った値をそのまま返却しています。

  const reducer = (state, action) => {
   // 受け取った値をそのまま戻り値として返す
    return action.content
  };

action.typeの値に応じて条件分岐する

先ほどのサンプルだと、dispatchされたactionデータを表示しているだけです。
今度は、actionのtypeプロパティの値で、処理を分岐してみます。

以下は、ボタンを押したらそれぞれ異なる値が表示される簡単なサンプルです。

See the Pen react-reducer2 by donguri2020 (@m-ke) on CodePen.

action.typeの値を使い、switch文で条件分岐します。switch文は日頃使わないので使い方を忘れていました、、。条件に一致しない処理であるdefaultの記述を忘れずに!

最終的にreturnでstateの値を更新します。
以下は、サンプルからswitch文を抜き出したコードです。

const reducer = (state, action) => {
  switch(action.type) {
    case "ACTION_TYPE":
      return action.content
    case "ACTION_TYPE_NUMBER" :
      return action.content
    default:
      state
  }
};

今回は、同じ値(action.content)でstateを更新していますが、値を加工したり、色々なことができそうです。

【サンプル】 TODOリストを作ってみる

具体的な実装例として、TODOリストを作りました。

以下は、TODOリストが追加されるまでの流れです。
図で表すと次のようになります。多分!!

上記の動きを整理すると、次のようになります。

  1. stateの初期値
    オブジェクトを値に持つ配列[{ id: 0, text: “TODOその1(初期値)” }]を設定
  2. アクションが発生
    TODOリストを入力して「追加」ボタンをクリック
  3. actionをdispatchする
    dispatchでactionオブジェクトがrecuder関数に送信される
    Reducerで受け取る引数は以下の2つ
    ・actionオブジェクト
    ・現在のstateの値
    ※actionの中身は{type: “ADDLIST”, text: “あいうえお”}
    ※stateの中身は[{ id: 0, text: “TODOその1(初期値)” }]
  4. reducerが実行
    action.typeの値が“ADDLIST”の場合、現在のstate(配列)に{id: 1, text: “あいうえお”}を追加してreturnで返却する
  5. reducerの戻り値でstateを更新
    stateが更新されたので、Viewが再レンダリングされてTODOが更新される。

【サンプル】チェックしたアイテムの合計金額が表示される

もう一つ、実際にありそうなシチュエーションでサンプルを作りました。
アイテムをチェックすると、合計金額がリアルタイムで更新されます。
※TypeScriptの練習も兼ねてます。

チェックしたアイテムはstate(中身は配列)で管理します。useReducerに関係する箇所を色付けしました。
チェックボックスのオンオフで異なるaction.typeがreducer関数に送信されます。reducer関数では、action.typeの値でアイテムの追加、または削除を行います。

最終的にreturnで返却した値でstateが更新されます。

App.tsx

import { useReducer } from "react";
import { ItemType, ActionType } from "./types";
import Items from "./components/Items";
import Result from "./components/Result";
import "./styles.css";

const checkboxItems: ItemType[] = [
  { id: 1, itemName: "Tシャツ", price: 2400 },
  { id: 2, itemName: "時計", price: 13000 },
  { id: 3, itemName: "ヘッドフォン", price: 2400 },
  { id: 4, itemName: "キャップ", price: 3000 },
  { id: 5, itemName: "サッカーボール", price: 3800 },
  { id: 6, itemName: "シューズ", price: 6800 }
];

// 価格表記
const createMoney = (price: number) => Number(price).toLocaleString();

// 初期値
const initItem: ItemType[] = [{ id: 0, itemName: "", price: 0 }];

// チェックしたアイテムを格納
const reducer = (itemList: ItemType[], action: ActionType) => {
  switch (action.type) {
    case "ADD":
      return [
        ...itemList,
        {
          id: action.id,
          itemName: action.itemName,
          price: action.price
        }
      ];
    case "DELETE":
      const deleteItemList = itemList.filter((item) => item.id !== action.id);
      return deleteItemList;
    default:
      return itemList;
  }
};

export default function App() {
  // useReducer 初期化
  const [itemList, dispatch] = useReducer(reducer, initItem);

  // チェックボックスをクリック
  const onCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
    const target = e.target;
    if (target.checked) {
      dispatch({
        type: "ADD",
        id: Number(target.dataset.id),
        itemName: target.dataset.name,
        price: Number(target.dataset.price)
      });
    } else {
      dispatch({
        type: "DELETE",
        id: Number(target.dataset.id)
      });
    }
  };

  return (
    <div>
      <p className="itemName">商品をチェックすると合計金額が反映されます</p>
      <Result itemList={itemList} createMoney={createMoney} />
      <Items
        onCheck={onCheck}
        checkboxItems={checkboxItems}
        createMoney={createMoney}
      />
    </div>
  );
}

合計金額は、アイテム情報から価格だけを抜き出した配列で計算しています。

Resut.tsx

  // アイテムリストから価格の配列を作成
  const itemListAry = itemList.map((item: any) => item["price"]);
  const totalPrice = itemListAry.reduce((accu: number, curr: number) => {
    return accu + curr;
  });

まとめ

今回作成したTODOリスト、アイテムの合計金額はuseStateでも実装できます。
では、どんな時にuseReducerを使うのが良いか、、?調べたところ、複雑なstateを更新するときに使うのがおすすめだそうです。理由としては、操作の窓口がdispatchだけになり、処理もreducer関数に集約できる為です。

管理するstateの数が多くなりそうだったら使ってみようと思います!

コメント