React学習:Reactフック編|useEffectとは?useLayoutEffectとの違いは?APIリファレンスを参考に徹底理解したい

    React学習記録|なんとなく理解禁止シリーズ
    React.js

Reactのなんとなく理解を(勝手に)禁止するシリーズでブログを書いています!

今回は、useEffectを分かったつもりにならずに、改めてAPIリファレンスを見返しながら徹底理解していきます。下記のような不安のある人に、少しでも参考になるかもです。

  • useEffectの使いどきがあやふや
  • 依存配列の使い方がいまいち分からん
  • useEffectuseLayoutEffectの違いが分からん
  • 依存配列に値を入れたのにESLintのWarningが出る

ESLintのWarning

Warning: React Hook useEffect has missing dependencies: '*****' and '*****'. Either include them or remove the dependency array. react-hooks/exhaustive-deps

useEffectとは外部システムと同期させるためのReactフック

そもそもuseEffectは避難ハッチの一つで、Reactのシステムの範囲外の外部システムと同期するために使うものです。

避難ハッチ(Escape Hatches)とは|Reactドキュメント

データフェッチをしたり、ブラウザから提供されるオブジェクト(document等)やWeb APIを活用したり、そういったReactのシステムから踏み出した外部システムと同期する際に使うためのReactフック。

もしそのコンポーネントがReactのサイクル内で済むならuseEffectは必要ない。」と公式のドキュメントにも説明がある通り、あくまでもReactのシステム外を見に行く際に活用するのがuseEffect

また、useEffectはクライアント上でのみ動作するもので、サーバレンダリング中には実行されません。この辺りの注意点については、公式リファレンスを参照すると良いです。

useEffect - 注意点|React API Reference

useEffectの使用例を見ながら基本を理解する

公式APIリファレンスの例を参照しながら、下記のポイントをまず理解していきます。

  • セットアップコード
  • クリーンアップコード
  • 依存値リスト

外部システムと接続する一般的な使用例

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    /**
     * セットアップコード
     */
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    // ---------------
  	return () => {
      /**
       * クリーンアップコード
       */
      connection.disconnect();
  	};
    /**
     * 依存値リスト
     */
  }, [serverUrl, roomId]);
  // ...
}

上記のソースは、./chat.jscreateConnectionの関数にサーバーのURLとチャットルームIDを渡して、チャットサーバーに接続をしている例になります。まずはソースにもコメントしている「セットアップコード」と「クリーンアップコード」について説明していきます。

セットアップコード|クリーンアップコード

セットアップコードはuseEffectが呼び出される際に実行される処理。クリーンアップコードは、コンポーネントがアンマウントされた際や再びuseEffectが実行される際の後始末担当のようなもので、useEffectの第1引数の戻り値として設定します。

例えば上記の例では、チャット接続をするコンポーネントがアンマウントされた際は、その接続を切断する必要があります。そのような処理を、useEffectの第1引数の戻り値として設定してやることで実装できます。

依存値リスト(依存配列)

依存値リストは、useEffectの処理によって使用される全てのリアクティブな値を含む配列です。依存配列に詰め込んだ値が変更されると、セットアップコードとクリーンアップコードが決められたサイクルに従って再実行されます。

一番の注意点は「依存配列は自分で選ぶ類いのものじゃない」ということ。依存配列に何を含めるかは、useEffectのソースによって絶対的に決定されます。

例えば、上記のチャット接続の例で言えば、[serverUrl, roomId]でなければいけません。もう一度ソースを見てみます。

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
}

serverUrlroomIdはこのコンポーネント内に含まれたリアクティブな値(データの変更を検知して自動的に更新される値)です。serverUrlsetServerUrlによって変更される可能性があるし、roomIdChatRoom()が実行されたタイミングで変更される可能性があるので、どちらもリアクティブな値と言えます。

それらのリアクティブな値を依存配列に入れないと、予期しないバグを生む可能性がある(バグの例)ので、依存配列はuseEffectのソースによって絶対的に決められるものとなります。

リアクティブな値には、props と、コンポーネント内に直接宣言されたすべての変数および関数が含まれます。roomId と serverUrl はリアクティブな値であるため、依存値のリストから削除することはできません。それらを省略しようとした場合、React 用のリンタが正しく設定されていれば、リンタはこれを修正が必要な誤りであると指摘します。
useEffect - リアクティブな依存配列の指定|React API Reference

もし、依存配列に何も含めたくない(マウント時に一回だけ実行させたい)場合は、それをソース上で証明する必要があります。つまり、serverUrlroomIdはリアクティブな値ではなく、これらを検知しなくて良いことを下記のように示します。

/**
 * リアクティな値でないことの証明
 */
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, []);
  // ...
}

ChatRoom関数の外に逃したり更新されない定数として扱うことで、serverUrlroomIdに依存する必要がなくなって、依存配列を空にすることができます。こんな形で、依存配列を空にしたいならソースを変えるべきで、無理やり空にしては本当はいけないんです。やっぱり公式ドキュメントは勉強になる...

依存配列を不適切に空にした時のESLintのWarningについて

ちなみに、チャット接続の例でリアクティブな値があるのにも関わらず、依存配列を設定しないと下記のようなWarningが出ると思います。

Warning: React Hook useEffect has missing dependencies: '*****' and '*****'. Either include them or remove the dependency array. react-hooks/exhaustive-deps

この時に下記のようにESLintを無理やり黙らせるやり方が記事とかでも出回っていると思いますが、やめた方が良いです。

useEffect(() => {
  // ...
  // 🔴 Avoid suppressing the linter like this:
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

これについては公式のセリフをそのまま載せておきます。

依存配列がコードと一致しない場合、バグが発生するリスクが高くなります。リンタを抑制することで、エフェクトが依存する値について React に「嘘」をつくことになります。代わりにそれらが不要であることを証明してください。

前述の通り、「依存配列は自分で選ぶ類いのものじゃない」ので、空を想定しているなら空で良いように(ESLintのWarningが出ないように)証明する必要があります。依存配列を取り除く例については、下記のドキュメントに詳細に書かれているので参考になります。

エフェクトから依存値を取り除く

useEffectのセットアップコードとクリーンアップコードのサイクル

依存配列が空の時のuseEffectサイクル

learning-react-hook-useeffect-001@1.5x.jpg

依存配列に何も設定されていなければ、useEffectのセットアップコードはコンポーネントがマウントされた後の1回のみに実行され、アンマウント時にクリーンアップが走ります。

依存配列が設定されている時のuseEffectサイクル

learning-react-hook-useeffect-002@1.5x.jpg

useEffectがリアクティブな値に依存していれば、その依存値が更新されたタイミングで ①クリーンアップ、②セットアップ の順で実行されます。クリーンアップはセットアップの処理をお掃除する役割なので、マウント後に実行された処理を先にお掃除する必要があるため、クリーンアップが先に実行されます。

useLayoutEffectとは?useEffectとの違いとは

useLayoutEffectは、Reactの「トリガー・レンダー・コミット」のプロセスの内、ブラウザがコミットを受け取って画面上に描画する前に「同期的に」実行されます。

そもそもuseEffectはブラウザ上に変更結果が描画される処理とは別軸で非同期的に実行されます。それぞれ下記の違いとメリットがあります。

useEffectuseLayoutEffect
ブラウザの描画とは別に
非同期的に実行
ブラウザの描画前に
同期的に実行
ブラウザの描画と連携してUI調整を
したい時にちらつくことがある
ブラウザの描画前に実行されるので
パフォーマンスを下げる恐れがある

useEffectが問題となるとき...例えばちらつき問題とは、下記のような現象です。

img-tech-blog-f3c4creot7dvgx6hra5p.gif

ボタンを押す度にランダムな数値を更新しているのですが、初期値がチラついて見えてしまっています。(目立つように悪魔の数字666👿にしましたが、下記ソースでは0を初期値にしてます)

function App() {
  const [state, setState] = useState(0)
  
  useEffect(() => {
    console.log('setup')
    state === 0 && setState(Math.random())
  }, [state])

  return <>
    <button onClick={() => {
      console.log('clickEvent')
      setState(0)
    }}>{state}</button>
  </>
}

ボタンがクリックされる度にstateが更新され、useEffectが発火します。初期値が0なのですが、useEffectでランダム数値に変更されて、ここらの処理が非同期的に行われることでチラついて見えてしまいます。

上記をuseLayoutEffectに変えてやればブラウザに描画されるよりも前にuseEffectという副作用処理を終えて、同期的にブラウザの描画に移行するので綺麗にランダム数値だけが表示されます。

useLayoutEffectのサイクルを見てみればより分かりやすいかもしれません。

useLayoutEffectのサイクル

learning-react-hook-useeffect-003@1.5x.jpg

useLayoutEffectの場合は、Updatedeと書いている箇所(公式的にはReactのrenderフェーズ)を全てやり切ってから、ブラウザ上に反映させるcomponentDidUpdateの処理に移ります。なので、useEffectによる余計なブラウザ描画が行われない訳です。

僕の出した例はチラつきを分かりやすくしたソースで正直そんな書き方をしなければ解決しますが...もっと現実味のある具体例は下記の公式APIリファレンスを参照してみてください。(ちょっと重めだけど...)

useLayoutEffect|React API Reference