はじめに
ReactのuseEffectについて、なんとなく使えている感はあったものの、個人開発でバグにはまって色々調べていくうちに、
これは立ち止まってしっかりインプットが必要だと感じたので、フロントエンド開発初心者の備忘録としてまとめてみます。
あくまで個人的にかみ砕いて、平易にまとめているので、詳細まで知りたい方は下記公式リファレンスも参照してみて下さい。
公式リファレンス-useEffectについて
※誤りありましたら、ご指摘いただけますと幸いです。
useEffectとは?
useEffectとは、Reactの関数コンポーネント内で副作用を扱うためのフックになります。
ちなみにフックとは、関数コンポーネントの中でReactの機能(状態管理や副作用など)を使えるようにする特別な関数のことを指します。
useEffectの説明で頻出する副作用という表現についても、ここで触れてみたいと思います。
副作用とは
副作用とは、簡単に言うと、コンポーネントのレンダリングの「外」で何かを行う処理になります。
例えば、
・APIからデータを取得する(フェッチ処理)
・setTimeout や setInterval等のタイマー処理
・イベントリスナーの登録や解除
・ローカルストレージへの保存・読み込み
これらの処理は、コンポーネントの描画処理とは別のタイミングで実行されるため、Reactでは「副作用」としてuseEffectでまとめて管理します。
もしもuseEffectを使わないと。。
Reactでは、コンポーネントが再描画(再レンダリング)される度に関数が実行されるため、外部への影響のある処理(いわゆる副作用)を直接コンポーネントに書いてしまうと、
何度も再レンダリングのたびに実行されてしまうという問題があります。
こういった副作用を適切なタイミングで一度だけ実行したり、条件に応じて再実行したりしたい時に、useEffectが必要になる、というわけです。
基本構文と使い方
useEffectは下記のように扱います。
useEffect(() => {
// 実行したい処理
}, [依存配列]);
例えば、マウント時に実行したい場合は下記のようになります。
useEffect(() => {
console.log("コンポーネントがマウントされました");
}, []); // 空配列によりマウント時のみ実行される
上記では依存配列に空配列を設定しています。
次はこの依存配列について解説していきます。
依存配列の扱い
useEffectでは、ある特定の値が変わった時にだけ副作用の処理を再実行するために、依存配列という仕組みを使います。
依存配列は簡単に言うと、**「この中に書かれた値が変わったときだけ、useEffectの処理を実行してね!」**という指定です。
依存配列の使い方とパターンについては、下記の通りになります。
書き方 | 動作 | 用途 |
---|---|---|
useEffect(() => { ... }, []) | 最初の一回だけ実行(マウント時) | 初期データの取得等 |
useEffect(() => { ... }, [value]) | valueが変わる度に実行 | 入力値の変化を監視したい場合、API再リクエスト等 |
useEffect(() => { ... }) | 毎回実行(再レンダリングのたび) | 基本的に非推奨(無駄な実行が多いため) |
まず、第二引数に空配列を指定した場合は、マウント時のみ実行されます。
ちなみに、マウントとは、簡単に言うと 「初回レンダリング+DOMが追加されるタイミング」 になります。
最初の一回と記述してあるのはそのような意味になります。
続いて第二引数に何かしら変数を指定した場合は、変数が変わる度 に実行されます。
上記の例ではvalueという変数を監視していますが、例えばuseStateなんかでvalueが更新されると、処理が走ります。
最後は第二引数に何も指定しない場合。
この場合はレンダリングのたびに毎回実行されます。
useEffectは副作用を扱うフックとなっていますので、非推奨となっているわけです。
【注意】第二引数に配列やオブジェクトを渡す場合
useEffectで第二引数に配列やオブジェクトを渡す際は、少し気を付けなければならない点があります。
例えば、下記のケース。
const [list, setList] = useState<number[]>([]);
useEffect(() => {
console.log('リストが変わった!');
}, [list]);
この場合、下記のように同じ配列を使い回して値を変更すると、再レンダリングは起こっても、useEffectは発火しません。
list.push(4); // 元の配列を直接変更している
setList(list);
これは、useEffectは変更があったと認識するのは参照が置き換わったタイミングであるためです。
そのため、下記のようにすることで、参照が変わったみなされ、useEffectが発火します。
setList([...list, 4]);
クリーンアップ処理について
クリーンアップ処理とは、useEffect 内で処理を実行した時に、
**「不要になった際に片付ける処理」**を指します。
例えば、
・タイマー(setInterval, setTimeout)の解除
・イベントリスナーの解除
・外部ライブラリの破棄
・WebSocketの切断
・API通信のキャンセル
このような処理がクリーンアップ処理に該当します。
先述の通り、Reactはコンポーネントを消したり再表示したりすることで最適化を進めているため、
副作用を放置しておくとメモリリークやバグの原因となってしまいます。
そのため、これら副作用を片付けるクリーンアップ処理が必要なんです。
基本構文は下記のようになります。
useEffect(() => {
// 副作用処理(例:イベントリスナーの登録)
const handleScroll = () => { console.log('スクロールした!') };
window.addEventListener('scroll', handleScroll);
// ⬇️ クリーンアップ関数を return で返す
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
上記のように、return に続く形でクリーンアップ処理を書くことができます。
ちなみに、クリーンアップ処理が走るタイミングは、
①コンポーネントのアンマウント時
②依存配列の値が変わる時
上記2点となります。
useEffectのタイミング
useEffectは **「ReactがDOMの更新を終えた後」**に実行されます。
仮想DOMを踏まえた簡単な流れは下記のようになります。処理を追ってみましょう。
- 状態(state)やpropsが変化
- Reactが仮想DOMを構築
- 前回の仮想DOMとの差分を比較
- 実際のDOM(ブラウザ)に必要な変更だけ反映
- useEffectが発火(DOMが更新された後に実行されている)
なぜこの順番なのかという問いへの答えは、実は先述してあります。
それは 「useEffectは副作用(描画が終わった後に行うべき処理)を扱うフック」 だからです。
耳にタコができるほど聞いた、この 副作用 という言葉の意味が少しずつ理解できてきた気がします。
useEffect vs useLayoutEffect
useEffectに似たフックに、 useLayoutEffect があります。
名前が似た両者ですが、大きな違いは実行されるタイミングにあります。
useEffectは画面描画後に実行されていましたが、useLayoutEffectは画面描画の直前に実行されます。
画面描画前に実行されるため、DOM操作やスタイル変更に適しているといえます。
例を見てみましょう。
useLayoutEffect(() => {
if (boxRef.current) {
boxRef.current.style.backgroundColor = 'red'; // 表示前に変更完了
}
}, []);
上記は背景色を変更するコードですが、このコード、useEffectで書くこともできます。
しかし、useEffectで書いた場合、画面描画が完了した後に背景変更処理が走ることから、
画面内で一瞬ちらつきが発生する可能性があります。
副作用の扱い等、あくまで基本的にはuseEffectで問題ないものの、DOM操作が絡むケースではuseLayoutEffectを使用する方が安全と言えます。
useEffectと非同期処理
useEffectでデータ取得を行いたい場合、非同期処理を使用したいと思います。
しかし、注意すべき点があります。
実際にコードを見てみましょう。
useEffect(async () => {
const res = await fetch('/api/data');
}, []);
2行目でfetchしており、awaitするため、この処理自体にasyncを設定しています。
しかし、実はこれはNGです。
正しいコードは下記になります。
useEffect(() => {
// 非同期関数を定義
const fetchData = async () => {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
};
fetchData(); // 非同期関数を呼び出す
}, []);
このように、第一引数に設定する関数自体をasyncにするのではなく、
第一引数内にasync関数を定義して、それを呼び出すことで解決します。
しかしなぜ、最初の書き方はNGになるのでしょうか?
この理由は、useEffectでは戻り値に、クリーンアップ処理が期待されているから です。
先述の通り、useEffectではreturnに続く形でクリーンアップ処理を記述できました。
しかし、第一引数の関数自体をasyncで定義すると、戻り値にはPromiseが期待されてしまいます。
そのため、非同期処理を行いたい場合は、第一引数内で別で関数定義を行い、呼び出す必要があるというわけです。
最後に
最近読んだ本の書籍で、時間をかけて理解することの重要さを学び、今回はじっくり理解することを目的に一からまとめてみました。
雰囲気だけ理解して技術を扱うと、チーム開発において思わぬ副作用をもたらす場合があるので、いい機会になりました。
個人的には、現場でVanilla.jsで開発を進めている最中に、突然useStateやらuseEffectといったReact Hooksが登場して挙動の違いに悩んだことがあったので、
今回でかなりクリアにできたと思います。
引き続きHooksは理解したいものが多くあるので、またまとめてみようと思います。