Open9

[React]useEffectをなるべく使わない実装方法

taihei_ataihei_a

How to remove unnecessary Effects ~ 不要なエフェクトを削除する方法

https://beta.reactjs.org/learn/you-might-not-need-an-effect#how-to-remove-unnecessary-effects

基本的な考え方を共有してる。

レンダリング用にデータを変換するためのuseEffectは不要

状態が更新されてからの基本的なコンポーネントの動きは以下。

  1. コンポーネントの状態を更新
  2. React は最初にコンポーネント関数を呼び出し
  3. 画面に何が表示されるかを計算
  4. 画面を更新
  5. React がuseEffectを実行

useEffectで画面に使う変数の更新をしてしまうとこの1~5のサイクルが複数回走ることになる。
レンダリング用にデータを変換するのは3の段階で行うべき。

ユーザー イベントを処理するためのuseEffectは不要

あまり使うイメージはできなかったけど、例で出しているところだと共通処理とかに使うイメージか?
原則としてユーザーイベント起因の処理はイベントハンドラーから呼び出されるべき。
共通処理があるのであれば関数を作ってその関数をイベントハンドラから呼び出す。

taihei_ataihei_a

Updating state based on props or state (PropsまたはStateに基づいて状態を更新する)

https://beta.reactjs.org/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-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(高価な計算のキャッシュ)

https://beta.reactjs.org/learn/you-might-not-need-an-effect#caching-expensive-calculations

PropsまたはStateに基づいて状態を更新するで紹介されているパターンでも問題ないが、再レンダリングが走る度に計算されてしまう。

前章で紹介されているような軽量の関数であれば問題ないが、
重い計算の場合は対象の値が更新された場合のみ再計算を行いたい。
そんな時はuseMemoを使って、計算結果をキャッシュする。

taihei_ataihei_a

Resetting all state when a prop changes (prop が変更されたときにすべての状態をリセットする)

https://beta.reactjs.org/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes

あるpropsが渡されたときに、状態をリセットしたい。
そんな時にはその監視対象のpropsをkeyをとしてコンポーネントに渡してあげる。

keyはmapで配列のプロパティをレンダリングするときにしか使わないイメージがあったけど、
コンポーネントの状態管理のために使えるという発想がなかったため新鮮。

taihei_ataihei_a

Adjusting some state when a prop changes(Propsが変更されたときにいくつかの状態を調整する)

https://beta.reactjs.org/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes

監視対象の以前の値を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);
  }
  // ...
}
taihei_ataihei_a

Chains of computations (計算の連鎖)

https://beta.reactjs.org/learn/you-might-not-need-an-effect#chains-of-computations

値の監視を連鎖的に行いたいとき、useEffectを連続に書いてしまうと見通しが悪く、柔軟性がないコードになってしまう。

ここでも紹介したように、useEffectの更新で状態が更新されると再レンダリングが走るので連鎖的に呼ばれる分だけ再レンダリングが走ってしまう。

  • 状態A の変更を監視して、useEffect A で 状態B の更新
  • 状態B の変更を監視して、useEffect B で 状態C の更新
  • 状態C の変更を監視して、useEffect C で 何らかの処理

解決策

イベントハンドラ内の実装で処理を書いてしまう。
動的にpropsの値を元に変化させたい値はコンポーネント内で計算する

taihei_ataihei_a

Initializing the application (アプリケーションの初期化)

https://beta.reactjs.org/learn/you-might-not-need-an-effect#initializing-the-application

Fetching data (データの取得)

https://beta.reactjs.org/learn/you-might-not-need-an-effect#fetching-data

コンポーネントの読み込み時に一度だけfetchしたいとき、fetch処理をuseEffectに書いていた場合、開発環境では2回呼び出しされ、意図しない挙動をしてしまうことがある。
https://qiita.com/yudong0114/items/9cc226f1efb37bc075d6

クリーンアップ関数を仕様するパターン。

taihei_ataihei_a

まとめ

方針は一貫している。
主に意識することが理解できたのでよかった。
個人的には以下のことを意識して開発していこうと思う。

  • 管理する状態(useState)を減らす
    • レンダリング中に計算できる値はstateとして保持せず、変数で定義する。
  • ユーザーイベント起因で発生する処理でuseEffectはなるべく使わない。