状態遷移図から理解するuseStateとuseReducerの違い
はじめに
React を始めた当初は、状態管理を useState で行っていました。
useReducer について調べても、「useState のただの上位互換でしょ。なんかわかりにくいし、使わなくていいっか」と思っていたので、数か月前までは全く触ってこなかったですが、概要がつかめてきたので、私が理解したことを簡単に説明していきたいと思います。
想定読者
- React における状態管理の概要を理解している方
- useState と useReducer を使うことはできるけど、違いが理解できていない方
- useReducer について改めて理解を深めたい方
前提条件
- React: 18.2.0
状態遷移とは
まずは、React から離れて、「状態遷移」とはどういったものかをざっくりと説明します。
状態遷移の知識を頭に入れてから React の状態管理のプログラムを見ると、useState や useReducer の違いがはっきりしてきます。
そもそも、状態遷移とは何でしょうか。
もしかしたら、状態遷移図とかをイメージされる方、多いのかもしれませんね。
そのイメージで正解です。
まずは、以下の状態遷移図を見てください。
パソコンの状態遷移図を表してみました。
図をかみ砕いて説明します。
まず、図の中の"状態"として、以下の 4 つが存在します。
- 「ユーザ画面」
- 「停止状態」
- 「ロック画面」
- 「スリープモード」
これらの"状態"と別に、『シャットダウンする』や『ログインする』、『一定時間経過する』などの"操作"もあることがわかります。
この"状態"と"操作"に関して、以下のようなことが言えます。
- 「停止状態」で『電源ボタンを押す』と「ロック画面」になります
- 「ロック画面」で『ログインする』と「ユーザ画面」になります
- 「ユーザ画面」で『シャットダウンする』と「停止状態」になります
これらを抽象化して"状態"と"操作"の関係を以下のように表現できます。
「前の"状態"」で『特の"操作"をする』と「次の"状態"」になる
実は状態遷移とは、この関係のことを言います。
useState や useReducer はこの状態遷移を管理するための関数ということを覚えておいてください。
useState での状態遷移の方法
やっと React の話です。
useState について、状態遷移の観点から見た useState の話をします。
useState の状態遷移の管理について、プログラムから復習しましょう。
import { MouseEvent, useState } from "react";
export default function Index() {
const [state, setState] = useState(1);
const onClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setState(state + 1);
};
return (
<div>
<p>{state}</p>
<button onClick={onClick}>+1</button>
</div>
);
}
これは、初期状態が 1 で、ボタンがクリックされたときに毎回+1 するプログラムになっています。細かい解説は割愛します。
ここで useState では状態遷移をどのように実装しているでしょうか。
実は、状態遷移を実施する際に、useState では「次の"状態"」を直接 setState 関数の引数に渡すことで、値を状態を更新しています。
実は useState は状態遷移における"操作"の部分をすっ飛ばして、状態を更新することができるのです。
useState は「次の"状態"」を直接指定して状態を更新できるため、とっつきやすく初心者に利用されやすいですが、
useState で生成された setState 関数 を利用するメンバーは全員、「次にどのような状態に遷移すべきか」を全て知っておく必要があることになります。
useReducer での状態遷移の方法
useState では、「次の"状態"」を直接指定して状態遷移をしていましたが、useReducer ではどうでしょうか。
まずは、useReducer の状態遷移の管理について、プログラムから復習しましょう。
import { MouseEvent, useReducer } from "react";
type Action = {
type: "count up";
};
export default function Index() {
const [state, dispatch] = useReducer((state: number, action: Action) => {
switch (action.type) {
case "count up":
return state + 1;
}
}, 1);
const onClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
dispatch({ type: "count up" });
};
return (
<div>
<p>{state}</p>
<button onClick={onClick}>+1</button>
</div>
);
}
実装内容は useState の時と同様ですが、どう違うかわかりますか?
実は、useReducer で状態を更新するとき、dispatch を利用しますが、指定しているのは"操作"内容になります。
状態遷移図を書くと以下の通りです。
あくまで、『'count up'という操作を実行してください』と記載しているだけで、「次の"状態"」は useReducer の引数で渡した第一引数の関数が担っています。
そのため、dispatch を利用する人は、「次の"状態"」に遷移するためにどのような計算をすべきかを知っておく必要はないのです。
useReducer メリットは、 dispatch 関数を利用するメンバーが、『dispatch が実施可能な操作』だけ知っていれば良いところにあります。
ここが useState との大きな違いです。
結論
useState と useReducer での状態遷移は以下のように違うことをご説明しました。
関数 | 状態遷移の方法 |
---|---|
useState | 「次の"状態"」を指定して状態を更新する |
useReducer | 「実行する"操作"」を指定して状態を更新する |
私個人としては、状態を遷移させる際に、都度「次の"状態"」を計算をしているとそこにバグが入ってしまいそうなので、
「実行する"操作"」だけを状態遷移時に指定する useReducer を基本的には使うべきと考えています。
とはいえ、今回のようなとても簡単な例ですと、useReducer が分量が増えてあまり恩恵が受けられないので、時と場合によりますが。。。
さいごに
useState と useReducer の違いについて、状態遷移という観点でお話ししました。
私自身このような記事を(数日前から)書き始めたばかりなので、読みにくい点や間違いが多いかもしれません。
もし、不明点や誤字脱字等ありましたら、コメントいただけますと幸いです。
Discussion