これまで useReducer をほとんど使ってきませんでした。理由は useState だけで特に問題にならなかったのと、useReducer は使いづらいイメージがあったためです。
- useReducer - フック API リファレンス – React
- An Easy Guide to React useReducer() Hook
useReducer はたいてい上記のように説明されます。ちなみに、後者のブログ記事は非常にわかりやすいので useReducer を全く知らない方はぜひご参照を。
Redux がやっていたような状態管理・更新を Hooks にしたようなもので、更新するには type 要素をもつ Action オブジェクトを渡します。
これが結構手間に感じられ、いつしか useReducer は頭のなかから消え、状態を持たせたい時は useState だけを使うようになっていました。
そんななか先日見かけた次の記事は目からウロコでした。
- A cure for React useState hell? - DEV Community 👩💻👨💻
- https://dev.to/builderio/a-cure-for-react-usestate-hell-1ldi
固定観念で useReducer は switch 文で action.type === 'hogehoge' して… みたいに使うものと捉えてしまっていましたが、確かに更新処理の引数には何を渡しても良いですね。
hooks での状態管理のかたち #
以下の3つのコードの書き方を載せました。1番目、2番目が useState を使用していて、3番目は先の記事に書いてある useReducer を使った書き方です。
import { useReducer, useState } from "react";
type Name = string;
type Profile = { firstName: Name; lastName: Name };
export default function App() {
  return (
    <>
      <Form1 />
      <Form2 />
      <Form3 />
    </>
  );
}
/**
 * 1. useState で要素数分の state を管理
 */
function Form1() {
  const [firstName, setFirstName] = useState<Name>("");
  const [lastName, setLastName] = useState<Name>("");
  return (
    <div>
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
    </div>
  );
}
/**
 * 2. useState でオブジェクトの state を管理
 */
function Form2() {
  const [profile, setProfile] = useState<Profile>({
    firstName: "",
    lastName: "",
  });
  return (
    <div>
      <input
        value={profile.firstName}
        onChange={(e) =>
          setProfile((prev) => ({ ...prev, firstName: e.target.value }))
        }
      />
      <input
        value={profile.lastName}
        onChange={(e) =>
          setProfile((prev) => ({ ...prev, lastName: e.target.value }))
        }
      />
    </div>
  );
}
/**
 * 3. useReducer でオブジェクトの state を生成
 */
function Form3() {
  const [profile, updateProfile] = useReducer(
    (prev: Profile, next: Partial<Profile>) => {
      return { ...prev, ...next };
    },
    { firstName: "", lastName: "" },
  );
  return (
    <div>
      <input
        value={profile.firstName}
        onChange={(e) => updateProfile({ firstName: e.target.value })}
      />
      <input
        value={profile.lastName}
        onChange={(e) => updateProfile({ lastName: e.target.value })}
      />
    </div>
  );
}
小さいコンポーネントに閉じた状態管理ならどんな形をとっても問題になることはほとんどありません。
ただし、あるべき姿はどれかと言われれば3番目です。
1番目は本来関連している項目を別々に宣言・管理してしまっているため、2番目、3番目のかたちに劣っています。
2番目と3番目を比較すると、2番目は更新処理を実行する側で ...prev とスプレッドしなければならないのに対し、3番目は宣言した側に閉じ込めることができるので優れています。
useReducer は無防備な更新関数を晒す必要がない #
useState と useReducer をもう少し見比べます。
たいていの更新処理にはバリデーションがあるでしょうから、setState をそのままイベントハンドラに渡すことは稀です。「useState で管理」のコード例にあるように、実際には更新用関数を定義してそちらを渡すかたちを取ると思います。
ただしこの場合でも state を無条件に更新できる setProfile 関数を呼ぶことでバリデーションを無視して更新できる可能性は残ってしまいます。
その点「useReducer で管理」のコード例では、更新用関数はバリデーション込みの useReducer のみです。誤って意図しない処理をされる可能性がなく優れています。
import { useReducer, useState } from "react";
type Name = string;
type Profile = { firstName: Name; lastName: Name };
export default function App() {
  return (
    <>
      <FormWithUseState />
      <FormWithUseReducer />
    </>
  );
}
/**
 * useState で管理
 */
function FormWithUseState() {
  const [profile, setProfile] = useState<Profile>({
    firstName: "",
    lastName: "",
  });
  const updateProfile = (next: Partial<Profile>) => {
    setProfile((prev) => {
      const newState = { ...prev, ...next };
      // バリデーション:firstName と lastName どちらかは必須
      if (newState.firstName.length === 0 && newState.lastName.length === 0) {
        return prev;
      }
      return newState;
    });
  };
  return (
    <div>
      <input
        value={profile.firstName}
        onChange={(e) => updateProfile({ firstName: e.target.value })}
      />
      <input
        value={profile.lastName}
        onChange={(e) => updateProfile({ lastName: e.target.value })}
      />
    </div>
  );
}
/**
 * useReducer で管理
 */
function FormWithUseReducer() {
  const [profile, updateProfile] = useReducer(
    (prev: Profile, next: Partial<Profile>) => {
      const newState = { ...prev, ...next };
      // バリデーション:firstName と lastName どちらかは必須
      if (newState.firstName.length === 0 && newState.lastName.length === 0) {
        return prev;
      }
      return newState;
    },
    { firstName: "", lastName: "" },
  );
  return (
    <div>
      <input
        value={profile.firstName}
        onChange={(e) => updateProfile({ firstName: e.target.value })}
      />
      <input
        value={profile.lastName}
        onChange={(e) => updateProfile({ lastName: e.target.value })}
      />
    </div>
  );
}
useState と同じように気軽に useReducer を使う #
無防備な setState を晒さなくて済むという利点を活用するためにも、対象の state が複雑(=オブジェクト)であるかに関わらず、もっと気軽に useReducer を使って良いと思います。
import { useReducer } from "react";
export default function App() {
  return <Form />;
}
function Form() {
  const [nickname, updateNickname] = useReducer(
    (prev: string, next: string) => {
      // ニックネームは 10 文字まで
      if (next.length > 10) {
        return prev;
      }
      return next;
    },
    "",
  );
  return (
    <input value={nickname} onChange={(e) => updateNickname(e.target.value)} />
  );
}
useReducer は switch 文や action.type のかたちで使うものという思い込み(私だけかもしれませんね)を捨てて、もっと気軽に使っていきましょう!