備忘録: ReactのuseEffectの実行条件とタイミング

React

はじめに

ReactのHookの中でも使用頻度の高いuseEffect。サンプルなど見ながら何となく使ってきたが、それでは心許なくなってきた。そこで公式ドキュメントを読みながら自分の理解を整理してみた。

筆者なりのuseEffectの理解は「Reactコンポーネントの中で、何らかの処理を、特定の条件で、特定のタイミングで、コールバックとして実行するための仕組み」というもの。何らかの処理は自分が書くものなので良いとして、その実行条件と実行タイミングについては、どのようなものか理解しておく必要がある。

なぜサイドエフェクトなのか?

いきなり脱線気味だが、筆者はこれが気になってしょうがない。

ReactのドキュメントにはuseEffectについて次のように書かれている。

The Effect Hook lets you perform side effects in function components:

これを初めて読んだときには正直「んんんっ?」となってしまった。「副作用? 意図しない動作ということ?」経験上、副作用には何かトラブルの臭いがする。それにサイドがあるならメインがあるはずだけど、この場合のメインってなんだろう?

疑問なのは筆者だけ? でも名は体を現すとも言うし、名前の意味 (意図) が理解できないと、その後の説明が頭に入ってこない …

これについては次のように理解した。Reactコンポーネントは、画面表示のためのJSXを作成して返す。このJSXを返す動作がメインとなる。これに対してuseEffectで登録されたコールバックでは、JSXを作成したり返したりすることはない。その意味でJSXを返すというメインの動作には関係しない。なのでサイドエフェクトということかと。

あまりしっくりこないけど、まあ当たらずとも遠からずだろうと思いたい …

コールバックの登録

useEffectは、Reactコンポーネントの関数定義の中に書かれる。つまりコールバックの登録は、Reactコンポーネントが呼び出された時に行われる。(当たり前といえば当たり前だが …)

ならばReactコンポーネントはいつ呼び出されるのか。それは下記のタイミングとなる。

  • 上位のコンポーネントから呼び出された時
    • 上位のコンポーネントのJSX定義でネストされている
  • コンポーネントのステートが変更された時
    • 正確にはステートの変更を引き起こす何らかのイベントが発生したとき
      • マウスなどのUIイベント、ネットワークI/Oの完了イベントなど
      • useEffectのコールバックの処理や上位コンポーネントからの呼び出しもステートの変化を引き起こす場合がある

コンポーネントが呼び出されるたびにコールバックが渡されて、古いものを置き換えていく。

コールバックの実行タイミング

useEffectに登録したコールバックが実行されるのは画面の描画後となる。

ドキュメントと簡単なテストプログラムから、レンダリングは次のようなサイクルで実行されていると推測した。(公式ドキュメントで確認できた訳では無いので、大間違いしている可能性もある …)

プログラムコードの中で行うのはJSXの生成まで。そのあとはReactのランタイムの中で処理される。useEffectや後述するuseLayoutEffectで登録されるコールバックの実行はランタイムの中である。

コールバックの実行条件

前述したように、useEffectのコールバックが実行されるのは、Reactコンポーネントが呼び出され、Reactによる画面の描画が行われたあとになる。ただし毎回の描画後に必ず呼び出される訳ではなく、呼び出しの条件を指定することができる。

  • 毎回必ず
  • 初めてコンポーネントが呼び出されたとき (初回限り)
  • 指定した条件のとき

「毎回必ず」は要注意。コールバックでステートを変更すると、それにより再描画がおこり、コールバックが再び呼び出される。もしステートの更新が続くとコールバックの呼び出しが無限ループとなってしまう。これは「指定した条件のとき」も、同様のことが起こり得る。

条件の指定は、useEffect()を呼び出す時の第2引数に指定する。

  • 何も指定しなければ毎回必ず
  • 空の配列を指定すると初回限り
  • 空でない配列を指定すると、その何れかの要素が更新されたとき
    • 前回 useEffect()が呼び出された時に渡された値と異なるとき

にそれぞれコールバックが呼び出される。

コールバックからのUIの更新

useEffectのコールバックでは、JSXによるUIを直接*変更できないが、DOMを直接操作することでUIを変更できる。ただしReactの設計思想として、DOMの直接操作はできるだけ避けるべしとなっている。

* 直接は変更できなくても、ステートを更新することで、間接的に変更することはできる (これもサイドエフェクトの由縁か …)

フリッカーの発生

useEffectでDOMを直接更新すべきでない理由のひとつとしてフリッカーの発生がある。コンポーネントの通常処理で画面が描画されたあとに、更に画面を更新することで、短時間に表示が変化することになり、それがフリッカーとなる。このフリッカーを防ぐには、後述するuseLayoutEffectを使用する。

ステートの更新によるUIの更新

「useEffectのコールバックではJSXによるUIの変更はできない」と書いたが、実際にはコールバックの処理結果で画面を更新したい場合がある。その場合はステートを更新して、コンポーネントの再呼び出しを行わせる。その呼出の中で、コールバックの処理結果 (例えばサーバーから取得したデータ) を使ってJSXを生成すればよい。

useLayoutEffectの使用

DOMを直接操作してUIを更新したい場合は、useLayoutEffectを使用する。useLayoutEffectはuseEffectとよく似ているが、違いは呼び出されるタイミングにある。

useLayoutEffectのコールバックは、JSXからDOMを構築したあと、描画の前に呼び出される。useEffectと違い、描画前にDOMを変更できるため、フリッカーを生じることがない。DOM上の要素はリファレンス (useRef) を使ってアクセスする。

useLayoutEffectのコールバックは、その処理が終了するまで、画面が描画されない。このためコールバックの処理に時間がかかると、遅いUIになってしまう。

複数のuseEffectの使用

useEffectは複数定義できる。useEffectの実行条件ごとに独立したuseEffectを定義したり、処理内容に応じて複数のコールバックを用意したりできる。

コールバックの後処理

useEffectのコールバックで何らかの処理を開始して、その後始末をしたい場合がある。この場合は、コールバックから後処理の関数 (以下クリーンナップ) を返してやればよい。クリーンナップは、次のサイクルでコールバックが呼ばれる前に実行される。つまり何らかのトリガーで再描画が発生した場合に実行される。もし再描画が発生しなければ、クリーンナップは実行されない 。

コールバックの中でステータスを変更し、その結果を画面に反映し、その後に後処理を行う、というサイクルが想定されていると思われる。

async/await

useEffectのコールバックでasync/awaitを使うには次のように記述する。

useEffect(() => {
  doSomething(...);
}

async doSomething(...) {
  await doAsync(...);
}

筆者はTypeScriptを使っており、最初下記のように記述したが、useEffectはPromiseの戻り値を受け付けないと、TypeScriptに怒られてしまった。

// エラーが発生する書き方
useEffect(async () => {
  await doAsync(...);
}

doAsync()の処理は、レンダリングのサイクルとは切り離されて、非同期に実行されることになる。こうなると、あえてuseEffectのコールバックの中で起動する必要も無いかも知れないが …

その他 (雑感)

useEffectのコールバックが画面描画の後に呼び出されるのはどういう意図からだろう。

筆者の場合、リモートサービスからのデータの読み出しコードをuseEffectに書くことが多い。ただこれは非同期処理なので、画面の描画後という起動タイミングは重要ではない。起動タイミングだけを見ればメインロジックに記述しても問題ない。ただ起動条件の記述は、useEffectを使うと簡単にできるので、useEffectを使うメリットを感じている。

更新記録

日付内容
2022/11/10初版リリース

コメント

タイトルとURLをコピーしました