useReducerについて改めて勉強してみた
React 標準の hooks はぼちぼち使ってきた自分ですが、useReducer
だけはどうもしっくり来ず使っていませんでした。
ということで、己の凝り固まった考え方を溶かすべく、useReducer
について勉強していこうと思います。
そもそも useReducer
とは
useState の代替品です
とのことなので、useState
で書けるものは useReducer
に書き換えることが可能ってことみたいです。
やはりイメージは Redux ですよね。
useReducer
のメリット
useReducer
の使用が useState
より好ましいとされるケースは以下の通りらしいです。
- 複数の値にまたがる複雑な state ロジックがある場合
- 前の state に基づいて次の state を決める必要がある場合
1 つずつ自分なりに解釈をば。
複数の値にまたがる複雑な state ロジック
たとえば自分はよく以下のようなイメージのコードを書いていました。(動作未確認ですが…)
function Hoge(): JSX.Element {
const [count, setCount] = useState(0);
const [isEven, setIsEven] = useState(!(count % 2));
const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
useEffect(() => {
setIsEven(!(count % 2))
}, [count]);
return (
<>
<button onClick={handleClick}>
INCREMENT COUNT
</button>
<div>{`Count is ${isEven ? "even" : "odd"}.`}</div>
</div>
);
}
この場合、state
を 2 つ切ることになるのでちょっと記述が冗長的ですよね。
で、これを useReducer
で書き直すと。
const initialState = {
count: 0,
isEven: true
}
function reducer(state, { type }) {
switch (type) {
case "increment": {
const { count } = state;
const nextCount = count + 1;
return {
count: nextCount,
isEven: !(isEven % nextCount)
};
}
default: {
return state;
}
}
}
function Hoge(): JSX.Element {
const [{ isEven }, dispatch] = useReducer(reducer, initialState);
const handleClick = useCallback(() => {
dispatch({ type: "increment" })
}, []);
return (
<>
<button onClick={handleClick}>
ADD COUNT
</button>
<div>{`Count is ${isEven ? "even" : "odd"}.`}</div>
</div>
);
}
といった感じになりますかね?
state
の管理をコンポーネントの外側で行うため、いわゆる Redux っぽい考え方になると思います。
で、個人的におもしろいなーと思うのが、useEffect
が不要になっていることですね。
今までの書き方だと count
の副作用によって isEven
の値を書き換えざるを得なかったのですが。
useReducer
の場合 state
をオブジェクトで持つことがほとんどだと思いますので、結果的に副作用として働かせる必要がなくなるということですね。
前の state に基づいて次の state を決める必要
ちょっと表現があいまいでわかりづらいですが、公式ドキュメントの useState
ところに書いてある補足のことだと思っています。
クラスコンポーネントの setState メソッドとは異なり、useState は自動的な更新オブジェクトのマージを行いません。この動作は関数型の更新形式をスプレッド構文と併用することで再現可能です:
const [state, setState] = useState({});
setState((prevState) => {
// Object.assign would also work
return { ...prevState, ...updatedValues };
});
別の選択肢としては useReducer があり、これは複数階層の値を含んだ state オブジェクトを管理する場合にはより適しています。
複数階層の値を含んだ state オブジェクト
というのが引っかかりやすいポイントだと思いますが。
スプレッド構文による複製はシャローコピー、つまりネスト 1(= 1 段階の深さ)のみ行われます。(スプレッド構文)
したがって 複数階層の値を含んだ state オブジェクト
を useState
を用いて更新する場合、コンポーネントに複雑な更新ロジックを持たせることになってしまいます。
となれば、無理にコンポーネントの内部で解決せず、useReducer
を使って更新ロジックを外出しにしたほうが良い、ということなのかなーと思っています。
useReducer
による最適化
また、useReducer を使えばコールバックの代わりに dispatch を下位コンポーネントに渡せるようになるため、複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化にもなります。
文脈通りに受け取ると dispatch
関数を props
として流し込んでしまいそうになりますが、これは間違った解釈です。
我々は個別のコールバックを props として渡すのではなく、コンテクスト経由で dispatch を渡すことを推奨しています。
https://ja.reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down
大きなコンポーネントツリーにおいて我々がお勧めする代替手段は、useReducer で dispatch 関数を作って、それをコンテクスト経由で下の階層に渡す、というものです。
手順としては以下の通りですね。
useReducer
を呼び出し、dispatch
関数を定義するcreateContext
を呼び出し、Provider
にdispatch
関数を流し込む- 子コンポーネントで
useContext
を呼び出し、dispatch
関数を取得する
この方法によるメリットも書かれていますね。
dispatch のコンテクストは決して変わらないため、dispatch だけを使うコンポーネントは(アプリケーションの state も必要でない限り)再レンダーする必要がなくなります。
useReducer
によって取得される dispatch
関数は不変になるため、パフォーマンスの向上が望められるってことですね。
ただし state
もコンテキストに渡したい場合は注意が必要なようです。
もしもコンテクストを使って state も渡すことにする場合は、2 つの別のコンテクストのタイプを使ってください
とはいえ、個人的にはあまり推奨したいやり方ではないです。
単純に Redux の useDispatch
と useSelector
を好き勝手に呼び出したら大変なことになるよね?って話と同じなわけで。
我々が見たところ、ほとんどの人はコンポーネントツリーの各階層で手作業でコールバックを受け渡ししていく作業が好きではありません。それはより明示的ではありますが、面倒な『配管工事』をしている気分になることがあります。
と書かれている通り、明示的ではなくなりますし保守難易度がぐっと上がります。
基本的には愚直かつ明示的に props で流し込んでいきましょう。
そんな感じみたいです、やはり扱うのはかなり難易度が高そうですね。
他にも遅延初期化に関するドキュメントもあるようですが、そこまで頻繁に扱うものでもないと思うのでスルーします。
公式ドキュメントでは比較的 useReducer
に関するメリットばかり書かれていましたが(当たり前だけど)、個人的な経験としてはデメリットも結構大きいです。
reducer
をどこに定義するのか
useState
と異なり、reducer
はコンポーネントの外側に定義する必要があります。
となると reducer
ってどこに書けば良いのか、個人的にはかなり難しいよなーと。
腕の見せ所になりそうな気はしますね。
context
も連発させるものじゃない
「dispatch
関数を context
で流し込めば良いよ!」って書かれていますが、実際問題そんな簡単な話ではないです。
小規模なプロジェクトであればなんとでもなりますが、中規模〜大規模なプロジェクトになってくるとやはりコンポーネント設計が重要です。
そこで子コンポーネントに対して context
で流し込むといった手法を使っていると、もうもうもう保守なんてまったくできなくなります。
そういった記述をしているプロジェクトを何度も見てきましたが、本当にやめましょう。
基本は props
でバケツリレー、これは絶対です。
基本は useState
を使えば良い
調べる前は『state
にオブジェクトを扱いたいケースでは useReducer
を使えば良いのかな?』と思っていたんですが、そんな感じでもない印象を受けました。
ぶっちゃけシャローコピーで解決が十分なケースも多いと思いますし、基本は useState
で良さそうです。
となると useReducer
の使いみちって、フロントでよほど複雑な state
と更新ロジックを持つケースに限定されそうですよね。
最後に、『個人的にこういうケースでは useReducer
は使えるかも』というのを書いておきます。
- カスタム hooks 内で呼び出す
_app
のようなもっとも大枠のコンポーネントから流し込む(認証系とかで使えそう)- 呼び出し回数が比較的少ないグローバルなコンポーネントで受け取る(ヘッダーやメニューなど)
- presentational component において UI に関する複雑な state を扱う
公式以外の日本語のドキュメントも色々と読んでみましたが、正直どれもあまりピンと来ず…。
英語ではありますが An Easy Guide to React useReducer() Hook という記事がもっともわかりやすかったです。
結局 useReducer
は Redux だなーと強く感じさせられました、あまり良い使いどころはない印象です。
繰り返しになりますが、下手に書くと保守難易度が上がっていく一方になってしまうので基本は useState
で十分です。
とはいえ便利な hooks であるのも間違いないので、自分もうまいこと使ってみたいなーと思った今日このごろです。