【React】useReducerの使い方

こちらの書籍より学ばせていただいたことをまとめています。

書籍の説明

書籍名: Reactハンズオンラーニング 第2版 ―Webアプリケーション開発のベストプラクティス
著者: Alex Banks 著
出版社: オライリージャパン 発行
発売日: 2021/8/6

複雑なstateを簡略化することができるhookであるuseReducerについてまとめました。

チェックボックスをON/OFFするこちらのコンポーネントを例に見ていきます!

import { useState } from "react";

export const Checkbox = () => {
  const [checked, setChecked] = useState(false);

  return (
    <>
      <input
        type="checkbox"
        value={checked}
        onChange={() => setChecked((checked) => !checked)}
      />
      {checked ? "checked" : "not checked"}
    </>
  );
};

これには、一点改善が必要な箇所があります。

それがこちら

onChange={() => setChecked((checked) => !checked)}

この部分で、わざわざstateを引数として受け取っていますが、値を反転させたいだけなので引数を受け取る必要はありません。

そこで、toggleという名前で関数を作り、そこでstateを反転させます。

import { useEffect, useState } from "react";

export const Checkbox = () => {
  const [checked, setChecked] = useState(false);

  const toggle = () => {
    setChecked((checked) => !checked);
  };

  return (
    <>
      <input type="checkbox" value={checked} onChange={toggle} />
      {checked ? "checked" : "not checked"}
    </>
  );
};

(checked) => !checked この部分をReducerと呼びます。

Reducerは、現在のstateを受け取り、新しいstateを返す関数のことをいいます。

ここにhookのuseReducerが使えるというわけです。

useStateの代わりにuseReducerを使うことで、state更新のロジックを抽象化することができます。

これがuseReducerを使うことのメリットの一つです。

ではどういう意味か見ていきましょう!

import { useReducer } from "react";

export const Checkbox = () => {
  const [checked, toggle] = useReducer((checked) => !checked, false);

  return (
    <>
      <input type="checkbox" value={checked} onChange={toggle} />
      {checked ? "checked" : "not checked"}
    </>
  );
};

useStateがuseReducerに書き換わり、関数toggleがuseReducerの第2引数になっています。

useReducerは”Reducerの関数”と”stateの初期値”を引数にとります。

useReducerの戻り値の配列は、1番目の要素がstate値で、2番目の要素がReducerを実行するための関数(dispatch関数)となります。

Reducerは同じ引数を呼び出された場合、必ず同じ戻り値を返します。

以上が、useReducerの使用方法です。

しかし、これでは正直useReducerを使うメリットが感じられませんね。

この時点では、stateの値を変えるReducerをuseReducerを使って書き換えたというくらいの理解ではないでしょうか。

useReducerを使った複雑なstate管理

useReducerは、より複雑なstateを更新する際に効力を発揮しますので、その例を見ていきましょう!

次のような、複数のユーザー情報をstateとして持つ、Userコンポーネントの場合を考えます。

import { useState } from "react";

const firstUser = {
  firstName: "Bill",
  lastName: "Wilson",
  admin: false,
};

function User() {
  const [user, setUser] = useState(firstUser);

  return (
    <div>
      <h1>
        {user.firstName} {user.lastName} - {user.admin ? "Admin" : "User"}
      </h1>
      <button
        onClick={() => {
          setUser({ admin: true });
        }}
      >
        Make Admin
      </button>
    </div>
  );
}


この場合、setUser({ admin: true }) とすることで、firstUserのプロパティ全て(firstName, lastName, admin)を書き換えてしまうことになります。

adminの値だけを書き換えたい場合、正しくは、スプレッド構文を使用して、既存のstateを展開してから、更新されたプロパティのみを上書きするという以下の記述とする必要があります。

      <button
        onClick={() => {
          setUser({ ...user, admin: true });
        }}
      >

このミスは、バグを起こすもとになります。

ここでuseReducerの登場です。

useReducerを使って、useStateを書き換えます。

  const [user, setUser] = useState(firstUser);

      ↓

  const [user, setUser] = useReducer(
    (user, newDetails) => ({...user, ...newDetails}),
    firstUser
  )

Reducer関数には、userとnewDetailsという2つの関数を取ります。

userには現在のstate値が、newDetailsにはsetUserで指定された値が入ります。

これによって、最初に記述した、以下の形で書くことができるようになりました!

      <button
        onClick={() => {
          setUser({ admin: true });
        }}
      >

これで、間違いが起こりにくくなりました。

このように、更新関数に渡す値をコントロールできるのはuseReducerを使う二つ目のメリットです。

useReducerを使った複雑なstate管理の例(その2)

以下のような、-をクリックすると数値が減り、+をクリックすると増えるカウンターがあったとします。

import React, { useState } from "react";

export const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
    </div>
  );
};

これをuseReducerを使って、更に2倍とリセットの機能を追加したコードを書くと、、

import React, { useReducer } from "react";

const reducer = (currentCount, action) => {
  switch (action) {
    case "DECREMENT":
      return currentCount - 1;
    case "INCREMENT":
      return currentCount + 1;
    case "DOUBLE":
      return currentCount * 2;
    case "RESET":
      return 0;
    default:
      return currentCount;
  }
};

export const Counter = () => {
  const [count, dispatch] = useReducer(reducer, 0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch("DECREMENT")}>-</button>
      <button onClick={() => dispatch("INCREMENT")}>+</button>
      <button onClick={() => dispatch("DOUBLE")}>x2</button>
      <button onClick={() => dispatch("RESET")}>Reset</button>
    </div>
  );
};

なんと!onClickイベントの関数のところがdispatchに置き換えられ、複雑な関数をシンプルにすることができました。

また、ロジックをコンポーネントの外に出しているため、テストも容易になります。

まとめ

useReducerはstateを更新するためのhookであり、機能としてはuseStateの上位互換です。

基本形

const [state, dispatch] = useReducer(reducer, initialState)

以上、読んでいただきありがとうございました。

コメント