概要
この記事は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さん
は野菜嫌いなので野菜料理を選択した場合はエラーを吐くよう実装しています。
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さん
の野菜嫌いを知りません。
// 省略
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})
このように呼び出している点です。
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は有効な選択肢となると思います。