Reactのなんとなく理解を(勝手に)禁止するシリーズでブログを書いています!
今回は、useEffect
を分かったつもりにならずに、改めてAPIリファレンスを見返しながら徹底理解していきます。下記のような不安のある人に、少しでも参考になるかもです。
useEffect
の使いどきがあやふや- 依存配列の使い方がいまいち分からん
useEffect
とuseLayoutEffect
の違いが分からん- 依存配列に値を入れたのに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.js
のcreateConnection
の関数にサーバーの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]);
// ...
}
serverUrl
とroomId
はこのコンポーネント内に含まれたリアクティブな値(データの変更を検知して自動的に更新される値)です。serverUrl
はsetServerUrl
によって変更される可能性があるし、roomId
もChatRoom()
が実行されたタイミングで変更される可能性があるので、どちらもリアクティブな値と言えます。
それらのリアクティブな値を依存配列に入れないと、予期しないバグを生む可能性がある(バグの例)ので、依存配列はuseEffect
のソースによって絶対的に決められるものとなります。
リアクティブな値には、props と、コンポーネント内に直接宣言されたすべての変数および関数が含まれます。roomId と serverUrl はリアクティブな値であるため、依存値のリストから削除することはできません。それらを省略しようとした場合、React 用のリンタが正しく設定されていれば、リンタはこれを修正が必要な誤りであると指摘します。
useEffect - リアクティブな依存配列の指定|React API Reference
もし、依存配列に何も含めたくない(マウント時に一回だけ実行させたい)場合は、それをソース上で証明する必要があります。つまり、serverUrl
とroomId
はリアクティブな値ではなく、これらを検知しなくて良いことを下記のように示します。
/**
* リアクティな値でないことの証明
*/
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
関数の外に逃したり更新されない定数として扱うことで、serverUrl
とroomId
に依存する必要がなくなって、依存配列を空にすることができます。こんな形で、依存配列を空にしたいならソースを変えるべきで、無理やり空にしては本当はいけないんです。やっぱり公式ドキュメントは勉強になる...
依存配列を不適切に空にした時の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
サイクル
依存配列に何も設定されていなければ、useEffect
のセットアップコードはコンポーネントがマウントされた後の1回のみに実行され、アンマウント時にクリーンアップが走ります。
依存配列が設定されている時のuseEffect
サイクル
useEffect
がリアクティブな値に依存していれば、その依存値が更新されたタイミングで ①クリーンアップ、②セットアップ の順で実行されます。クリーンアップはセットアップの処理をお掃除する役割なので、マウント後に実行された処理を先にお掃除する必要があるため、クリーンアップが先に実行されます。
useLayoutEffect
とは?useEffect
との違いとは
useLayoutEffect
は、Reactの「トリガー・レンダー・コミット」のプロセスの内、ブラウザがコミットを受け取って画面上に描画する前に「同期的に」実行されます。
そもそもuseEffect
はブラウザ上に変更結果が描画される処理とは別軸で非同期的に実行されます。それぞれ下記の違いとメリットがあります。
useEffect | useLayoutEffect |
---|---|
ブラウザの描画とは別に 非同期的に実行 | ブラウザの描画前に 同期的に実行 |
ブラウザの描画と連携してUI調整を したい時にちらつくことがある | ブラウザの描画前に実行されるので パフォーマンスを下げる恐れがある |
useEffect
が問題となるとき...例えばちらつき問題とは、下記のような現象です。
ボタンを押す度にランダムな数値を更新しているのですが、初期値がチラついて見えてしまっています。(目立つように悪魔の数字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
のサイクル
useLayoutEffect
の場合は、Updatede
と書いている箇所(公式的にはReactのrender
フェーズ)を全てやり切ってから、ブラウザ上に反映させるcomponentDidUpdate
の処理に移ります。なので、useEffect
による余計なブラウザ描画が行われない訳です。
僕の出した例はチラつきを分かりやすくしたソースで正直そんな書き方をしなければ解決しますが...もっと現実味のある具体例は下記の公式APIリファレンスを参照してみてください。(ちょっと重めだけど...)