24
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

WanoグループAdvent Calendar 2022

Day 14

【React】useStateとuseReducer、使うのはどっち

Last updated at Posted at 2022-12-13

概要

この記事はWano Group Advent Calendar2022 14日目の記事です。
https://qiita.com/advent-calendar/2022/wano-group

WanoでVideo Kicksという
ミュージックビデオをiTunesやApple Music、レコチョクMUSICストア、dミュージック、GYAO!、LINE MUSICなどのビデオ配信ストアで配信/販売できるサービスを開発しています。

私は最近Reactを使い始めたので、useStateとuseReducerの違いと、どちらを使うべきかについて考えていきます。

環境

Vite,React,Typescriptを使用しています。

結論

基本はuseReducerを使った方がよさそう

理由は、stateが想定外の値で更新されることを未然に防ぐ事ができるからです。

ざっくり言うと、両者には以下のような違いがあります。

  • useState・・・state更新処理を使用する側が、更新方法や値を決める

  • useReducer・・・使用されるstate更新処理側が、更新方法や値を決める

useReducerであれば、state更新方法をあらかじめ決めておく事ができます。
使用する側はstate更新処理で使用する条件のみ渡せば良いため、極端に言うと更新方法は知る必要がありません。

一方で、useStateではstate更新関数の引数に、更新後の値を直接渡します。
従って、予想外の値が入ってきてエラーとなる可能性があります。

以降では、useState、useReducerのサンプルコードを比較してみます。

useState

まずは、useStateを使用したサンプルコードを以下に示します。
なお比較しやすいように、実装内容は後述のuseReducerのコードに寄せています。
これはMenuTypeという料理の種別を指定することで、今日の献立(state)を更新するコードです。

実装者であるAさんは、updateStateでstateの更新を行うつもりで実装しました。
このつもりというのが重要となります。
また、Aさんは野菜嫌いなので野菜料理を選択した場合はエラーを吐くよう実装しています。

menuState.tsx
import { useState } from "react";

const MenuType = {
  Chicken: "Chicken",
  Beef: "Beef",
  Fish: "Fish",
  Vegetable: "Vegetable"
} as const;

type MenuType = typeof MenuType[keyof typeof MenuType];

type Menu = {
  menu: string
};

const initialState: Menu = {menu: ""};

// 使用される側
const updateState = ({type: action}: {type: MenuType}): Menu => {
  switch (action) {
    case MenuType.Chicken:
      return {menu: "唐揚げ"};
    case MenuType.Beef:
      return {menu:"ハンバーグ"};
    case MenuType.Fish:
      return {menu: "寿司"};
    case MenuType.Vegetable:
      throw new Error("野菜は嫌いです");
    default:
      throw new Error("不正な値です");
  }
}

export const MenuState = () => {
  const [state, setState] = useState(initialState);

  return (
    <>
      <div>
        今日の献立: {state.menu}
      </div>
      <div>
        {/* 使用する側 */}
        <button onClick={() => setState(updateState({type: MenuType.Vegetable}))}>野菜</button>
        <button onClick={() => setState(updateState({type: MenuType.Chicken}))}></button>
        <button onClick={() => setState(updateState({type: MenuType.Beef}))}></button>
        <button onClick={() => setState(updateState({type: MenuType.Fish}))}></button>
      </div>
    </>
  );
}

野菜選択時は・・・

想定通りエラー吐いてますね!

その後、Bさんが次のような実装を行いました。BさんAさんの野菜嫌いを知りません。

menuState.tsx

// 省略

export const MenuState = () => {
  const [state, setState] = useState(initialState);

  return (
    <>
      <div>
        今日の献立: {state.menu}
      </div>
      <div>
        {/* 使用する側 */}
        <button onClick={() => setState(updateState({type: MenuType.Vegetable}))}>野菜</button>
        <button onClick={() => setState(updateState({type: MenuType.Chicken}))}></button>
        <button onClick={() => setState(updateState({type: MenuType.Beef}))}></button>
        <button onClick={() => setState(updateState({type: MenuType.Fish}))}></button>
        {/* ここに追加 */}
        <button onClick={() => setState({menu: "ピーマン"})}>ピーマン</button>
        {/* ここに追加 */}
      </div>
    </>
  );
}

するとどうなるか・・・

野菜の献立を禁止しているにもかかわらず、ピーマンが献立に選ばれてしまいました。
エラーも吐かず、正常な動作です。

これは、AさんupdateStateでstateの値を切り替えているつもりでしたが、
実際はsetStateの引数でstateを更新することになるので起こっています。

このようにuseStateでは、実装者にstate更新後の値が委ねられます。
一人や少人数であればよいですが、複数人で実装する際にAさんBさんのような認識齟齬が発生し、
想定外の値でstateが更新される恐れがあります。

useReducer

次に、useReducerのサンプルコードを以下に示します。

useStateと違うのは、stateの更新処理を

() => dispatch({type: MenuType.Vegetable})

このように呼び出している点です。

menuReducer.tsx
import { useReducer } from "react";

const MenuType = {
  Chicken: "Chicken",
  Beef: "Beef",
  Fish: "Fish",
  Vegetable: "Vegetable"
} as const;

type MenuType = typeof MenuType[keyof typeof MenuType];

type Menu = {
  menu: string
};

const initialState: Menu = {menu: ""};

// 使用される側
const reducer = (prev: Menu, {type: action}: {type: MenuType}): Menu => {
  switch (action) {
    case MenuType.Chicken:
      return {menu: "唐揚げ"};
    case MenuType.Beef:
      return {menu:"ハンバーグ"};
    case MenuType.Fish:
      return {menu: "寿司"};
    case MenuType.Vegetable:
      throw new Error("野菜は嫌いです");
    default:
      throw new Error("不正な値です");
  }
}

export const MenuReducer = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <div>
        今日の献立: {state.menu}
      </div>
      <div>
        {/* 使用する側 */}
        <button onClick={() => dispatch({type: MenuType.Vegetable})}>野菜</button>
        <button onClick={() => dispatch({type: MenuType.Chicken})}></button>
        <button onClick={() => dispatch({type: MenuType.Beef})}></button>
        <button onClick={() => dispatch({type: MenuType.Fish})}></button>
        {/* ここでエラー */}
        <button onClick={() => dispatch({type: MenuType.GreenPepper})}>ピーマン</button>
        {/* ここでエラー */}
      </div>
    </>
  );
}

さて、Bさんが再びピーマンボタンを実装しようとします。
しかし、IDE(画像はVSCode)の入力補完機能により、dispatchの引数に指定できる値が表示されます。

以下のように、想定外の値を渡そうとした場合はその時点でエラーを表示してくれるため実装時に気づく事ができます。

このように、useReducerでは使用される側(ここではreducer())がstate更新方法を決めています。
これによって、IDEの入力補完機能を通してBさんに対してstate更新方法を教える事ができます。

まとめ

特に複数人〜大規模開発では、全てのメンバーに正しく利用方法が伝わるような実装が理想的だと考えます。
今回のuseStateのように、一部の実装者しか知らないようなstate更新方法がある場合、それ以外の実装者への説明(野菜は禁止なのでピーマンを実装しないでほしい、など)が必要となってしまいます。

この工数を減らすためにuseReducerは有効な選択肢となると思います。

24
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?