まずuseReducerの使用を検討する
ただ、あまりにも遷移が自明な場合は、useReducerを使っても冗長になるだけ
そういう時に限りuseStateを使う
この自明さと状態遷移の制限の設け方の塩梅でどちらを使うかを決める
思考の順序としては
まずuseReducerで定義することを検討して、
ただ冗長になるけだなと感じる場合はuseStateを使う
という順番のほうが良さそうmrsekut.icon
(hooksとか関係なく)状態を扱う時にやりたいことは以下2つ
取りうる状態を全て型で明示したい
許される状態の遷移を全て明示したい
これが大前提mrsekut.icon*2
例えば、信号を表すものを考えてみる
取りうる状態は、Red, Yellow, Greenの3つ
これをStateの型として明示する
code:ts
type TrafficLight =
| { type: 'red' }
| { type: 'yellow' }
| { type: 'green'}
更に、これら3つの状態は、好きなように遷移できるわけではない
以下の3パターンの遷移しか取り得ない
red → green
green → yellow
yewllo → red
これをActionの型として明示する
このように、あらゆる状態と、その間の遷移のパターンを全て明示する
ただ、useReducerを使ったところで嬉しくならないケースもいくつかある
取りうる状態が無限にある場合
取りうる遷移のパターンが無限にある場合
取りうる状態や遷移が、型から自明に決定するとき
こういう時は、
useReducerで表現するのが無理だったり、
ただめんどいだけだったりするので
useStateを使ったほうが楽
取りうる状態や、遷移のパターンが無限にある場合
あるいは、列挙するのが面倒なぐらい多い場合
例えば、状態の型がnumberだったりすると、状態は無限にあるということになる
(本当に型はnumberで良いんですか?広すぎない?という話は別にある)
また、この状態が、
0から1に行ったり、1から40に行ったり、縦横無尽に遷移しうる場合は、
Actionとして列挙のしようがない
これが仮に、
「常に+1か-1する」のように取りうる状態のパターンが決まるなら、Actionとして明示できるし、そうするべき
取りうる状態や遷移が、型から自明に決定するとき
例えば、状態の型がbooleanだとすると、状態は2つに決まる
また、操作もtoggleの場合は、true → falseかfalse → trueしかなく、型から自明に決まる
これもreducerで書けるが、冗長になるだけで、全く嬉しみがないのでuseStateを使えば良い
状態はbooleanだが、
(toggleではなく)false → trueのみの遷移を許す
という場合は、reducerで表現するのが良さそう
逆に、コードの読み手からすれば、
useStateのまま提供されているのを見れば、
「この状態をどんなパターンで遷移してもおkなんだね」と解釈されるということ
また、当然、複数の状態を同時に扱うならパターンは更に増えるので制限の設け方が大事になってくる
上述の話の、特にActionの方に関しては、Custom Hookを定義することでも明示できる
許される状態遷移のための関数を定義して、それを公開する
利用者からすれば、「間違った状態遷移することはない」という観点では同じ効能を得る
これも、方針は同じで、
基本的にはuseReducerで定義することを検討して、
旨味が少ないなと感じるならuseState & custom hooksで定義すれば良さそう
reducerは状態に依存しない純粋な関数になるので、ロジックのテストをする際はこちらの方が圧倒的に扱いやすい また、この記事にもあるように、状態が表出しないおかげで、hooksのdepsに状態を書く必要がなくなる 故に、パフォーマンス改善に寄与する
React特有の嬉しさ(特徴)とも言える
useReducerは、instantiateした時点で取りうるパターンが決まっているのに対し、
useStateは、その時点では決まっておらず緩いので、custom hooksで制限しよう、という発想
これは例えば、状態を扱うcustom hooksがそこそこ大きい場合に差としてわかりやすい
100行程度以上あるcustom hooks内で、状態を扱うなら、useReducerの方が制限かけられるので嬉しい
(当然、hooksを分離すりゃ良いみたいな話は別にある)
useReducerを使うと冗長になる例
長い割に全く嬉しさがない
code:ts
type State = boolean;
type Action = { type: 'OPEN' } | { type: 'CLOSE' };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'OPEN':
return true;
case 'CLOSE':
return false;
default:
return exhaustiveCheck(action);
}
}
export const useModal = () => {
const open = useCallback(() => {
dispatch({ type: 'OPEN' });
}, []);
const close = useCallback(() => {
dispatch({ type: 'CLOSE' });
}, []);
return {
open,
close,
isModalOpen: state,
};
};
useStateを使うとこんなにスッキリ
code:ts
import { useState, useCallback } from 'react';
export const useModal = () => {
const open = useCallback(() => {
setIsModalOpen(true);
}, []);
const close = useCallback(() => {
setIsModalOpen(false);
}, []);
return {
open,
close,
isModalOpen,
};
};
でもまあ、useReducerの方もdispatchを直接公開するので良いと思う
code:ts
export const useModal = () => {
return {
dispatch,
isModalOpen: state,
};
};
呼ぶときが若干ダルいが、間違った呼び方はできないので情報量としては同じ
まず↑この辺の話が軸としてあって、その上でパフォーマンスがどうのこうのと言う話があると思うmrsekut.icon
↓こんな感じの「あるある説明」よく見るが、これは説明が雑すぎだし思考の順序が逆である
useReducerはいつ使えば良い?調べてみました!
複数の値にまたがる複雑な state ロジックがある場合
state自体が大きい場合
stateの更新のさせ方が複数ある場合
stateの更新のさせ方が複雑な場合
↑こういうときに使えば良さそうだということがわかりました!