[React]useEffectをなるべく使わない実装方法
この記事で紹介されているソリューションを理解していく
How to remove unnecessary Effects ~ 不要なエフェクトを削除する方法
基本的な考え方を共有してる。
レンダリング用にデータを変換するためのuseEffectは不要
状態が更新されてからの基本的なコンポーネントの動きは以下。
- コンポーネントの状態を更新
- React は最初にコンポーネント関数を呼び出し
- 画面に何が表示されるかを計算
- 画面を更新
- React がuseEffectを実行
useEffectで画面に使う変数の更新をしてしまうとこの1~5のサイクルが複数回走ることになる。
レンダリング用にデータを変換するのは3の段階で行うべき。
ユーザー イベントを処理するためのuseEffectは不要
あまり使うイメージはできなかったけど、例で出しているところだと共通処理とかに使うイメージか?
原則としてユーザーイベント起因の処理はイベントハンドラーから呼び出されるべき。
共通処理があるのであれば関数を作ってその関数をイベントハンドラから呼び出す。
Updating state based on props or state (PropsまたはStateに基づいて状態を更新する)
レンダリング用にデータを変換するためのuseEffectは不要で紹介されていたもの。
state、propsの状態を監視して、useEffectを書くことは基本必要ない。
state、propsが更新されたら、再レンダリングが走るのでコンポーネント内で再計算することで処理の高速化、見通しがよくなるなどの効果がある。
BAD
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
GOOD
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const fullName = firstName + ' ' + lastName;
}
Caching expensive calculations(高価な計算のキャッシュ)
PropsまたはStateに基づいて状態を更新するで紹介されているパターンでも問題ないが、再レンダリングが走る度に計算されてしまう。
前章で紹介されているような軽量の関数であれば問題ないが、
重い計算の場合は対象の値が更新された場合のみ再計算を行いたい。
そんな時はuseMemo
を使って、計算結果をキャッシュする。
Resetting all state when a prop changes (prop が変更されたときにすべての状態をリセットする)
あるpropsが渡されたときに、状態をリセットしたい。
そんな時にはその監視対象のpropsをkey
をとしてコンポーネントに渡してあげる。
key
はmapで配列のプロパティをレンダリングするときにしか使わないイメージがあったけど、
コンポーネントの状態管理のために使えるという発想がなかったため新鮮。
Adjusting some state when a prop changes(Propsが変更されたときにいくつかの状態を調整する)
監視対象の以前の値をprevItems
として管理することで、その差分を比較し差分があれば更新する。
というような実装。
公式もこれはあくまでBetterと言っているので別の解決方法があればそちらを採用する。
- コンポーネントをリセットする(
key
を使ってリセット) - コンポーネント内で再計算をする
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// Better: Adjust the state while rendering
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
Sharing logic between event handlers (イベント ハンドラ間でロジックを共有する
)
異なるイベントハンドラから更新された値を監視し処理を行いたいとき、
その処理をuseEffectに書くべきではない。
useEffectではなく、関数として切り出して処理を共通化させる。
Chains of computations (計算の連鎖)
値の監視を連鎖的に行いたいとき、useEffectを連続に書いてしまうと見通しが悪く、柔軟性がないコードになってしまう。
ここでも紹介したように、useEffectの更新で状態が更新されると再レンダリングが走るので連鎖的に呼ばれる分だけ再レンダリングが走ってしまう。
- 状態A の変更を監視して、useEffect A で 状態B の更新
- 状態B の変更を監視して、useEffect B で 状態C の更新
- 状態C の変更を監視して、useEffect C で 何らかの処理
解決策
イベントハンドラ内の実装で処理を書いてしまう。
動的にpropsの値を元に変化させたい値はコンポーネント内で計算する
Initializing the application (アプリケーションの初期化)
Fetching data (データの取得)
コンポーネントの読み込み時に一度だけfetchしたいとき、fetch処理をuseEffectに書いていた場合、開発環境では2回呼び出しされ、意図しない挙動をしてしまうことがある。
クリーンアップ関数を仕様するパターン。
まとめ
方針は一貫している。
主に意識することが理解できたのでよかった。
個人的には以下のことを意識して開発していこうと思う。
- 管理する状態(useState)を減らす
- レンダリング中に計算できる値はstateとして保持せず、変数で定義する。
- ユーザーイベント起因で発生する処理でuseEffectはなるべく使わない。