useEffectで詰まってしまったので理解を整理する目的でまとめてみた。
この記事は
useEffectとは何か
useEffectの使い方(3パターンだけ覚えよう)
useEffectのあるあるな対処法
の3本立てで気になるところを見てもらえれば。
useEffectとは何か
useEffect とは「React 16.8以降のversionで追加されたReact Hooksの機能の一つ。」のようなところは公式ドキュメントを見れば良いので省きます。
useEffect を一言でいうと レンダリング後メソッド のことです
(公式サイトでは副作用というより抽象的な単語が使われていて、詳細はReactにおける「副作用」とは?の記事がわかりやすかったです)。
言い換えると、 useEffect とは React の componentDidMount と componentDidUpdate 、 componentWillUnmount が一緒になった機能をもつ Hooks です。
加えて変数の変化も監視することができる Hooks が useEffect です。
useEffect と同じことを上の3つのライフサイクルメソッドでやろうとするとコードが重複して長ったらしくなります。
useEffect(() => {
console.log('useEffect');
}, [hoge])
ちなみに第一引数にあたる useEffect 内で呼び出されているコールバック関数、 () => {} のことを 副作用関数 と呼びます。
このコールバック関数はページがレンダリングされた後に呼び出されます。
第二引数にあたる hoge は監視したい変数を指します。
useEffectの使い方(3パターンだけ覚えよう)
useEffectはレンダリング後に実行される Hooks です。
ややこしそうな useEffect ですが使い方は主に3パターンだけですので基本的な使い方をするのであれば3パターンだけ覚えれば良いかと。
- 第二引数を省略するパターン
- 第二引数を空にするパターン
- 第二引数に任意の変数を設定するパターン
上の3パータン全てに共通することがあります。それは初回レンダリング後のタイミングで3パターンともに実行されるところです。
他のパターンには何があるの?と言うところですがあとは第二引数に複数の変数を設定するくらいでしょうか(それ以外の使い方を知りません)
そしてご存知のように初回レンダリング以外にも、さまざまなレンダリングのパターンがあり、全てにおいてuseEffect が実行されないように上の3パターンで使い分ける必要があります。
1. 第二引数を省略するパターン
useEffect が実行されるタイミング
・初回レンダリング後
・親コンポーネントの再レンダリング後(親のstateの変化だけではuseEffectは実行されない)
・propsの中身の変化後
・同じコンポーネント内のstateが変化した後
要するに子コンポーネントを除いたおおよそ全てのレンダリング時に実行されます。
下はタイマーとカウントのあるコードで 状態count と 状態msg の2つの状態を用意しています。お互い別々の機能です。
第二引数を省略しているとどうなるでしょうか。
(以降、下のコードをもとに他のパターンも解説)
import React, { useState, useEffect } from "react";
const TimerAndCount1 = () => {
const [count, setCount] = useState(0);
const [msg, setMsg] = useState("タイマー読み込み中");
useEffect(() => {
console.log("useEffect!");
startTimer();
});
const startTimer = () => {
return new Promise((resolve) => {
setTimeout(() => {
setMsg("タイマー読み込んだ");
}, 5000);
});
};
return (
<>
<h1>タイマーとカウント</h1>
<div>
<h2>タイマー</h2>
<div>{msg}</div>
</div>
<div>
<h2>カウント</h2>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>プラス</button>
</div>
</>
);
};
export default TimerAndCount1;
codeSandBox: https://codesandbox.io/s/peaceful-ishizaka-fbcbj?file=/src/TimerAndCount1.js
useEffect 内に5秒間のタイマー関数を実行させて実行後にメッセージ(msgの状態)を変化させるコードです。
加えて全く別にボタンを押すとカウントされる関数も用意しました。
useEffect 内に console.log("useEffect!"); を用意したので何回呼び出されるのか数えてみてください。
答えですが、まずページをロードした際に1回、そしてタイマー関数実行後に Msg の state が変化するので1回の計2回です。
あとは「プラス」ボタンをクリックした回数だけ count の state が変化するのでその回数だけ useEffect が呼び出されることになります。
2. 第二引数を空にするパターン
useEffect が実行されるタイミング
・初回レンダリング後
useEffect(() => {
console.log("useEffect!");
startTimer();
}, []);
先のコードサンプルに 空の第二引数 "[ ] "を追加。
これで初回レンダリング後のタイミングでのみ呼び出され、5秒後に実行される Msg の state の変化した後には呼び出されません。
当然 プラスボタン を何度クリックしても同様です。
3. 第二引数に任意の変数を設定するパターン
useEffect が実行されるタイミング
・初回レンダリング後
・countの状態の変化後
useEffect(() => {
console.log("useEffect!");
startTimer();
}, [count]);
先のコードサンプルに 空の第二引数 "[count] "を追加。
ちなみに第二引数に変数を設定する場合は , で区切って複数の変数を監視できます。
タイマーが呼び出された後に useEffect は呼び出されません。
useEffectのあるあるな対処法
初回レンダリング後のタイミングで呼び出したくない
実際に使っていて多かったのは初回レンダリング後にuseEffectを呼び出したくないパターンです。
対処法としては、
①第二引数に指定したstateの初期値の場合は条件分岐でスルー(初期状態が分かっている第二引数のstateが必要)
②useRefを使ったフラグを使う(どこでも使いやすい)
③react-useライブラリの useUpdateEffect を使う
①第二引数に指定したstateの初期値の場合は条件分岐でスルー(初期状態が分かっている第二引数のstateが必要)について、
const [count , setCount] = useState(0);
//省略
useEffect(() => {
if(count === 0) return;
console.log("useEffect!");
startTimer();
}, [count]);
useStateの初期値が分かっているため同じ値で条件分岐してそのまま return で返してあげれば読み込まれません。
②useRefを使ったフラグを使う(どこでも使いやすい)について、
import React, { useState, useEffect, useRef } from "react";
//省略
const firstTimeRender = useRef(true)
useEffect(() => {
if(!!firstTimeRender.current) {
firstTimeRender.current = false;
return;
}
console.log("useEffect!");
startTimer();
});
useRefのboolを変化させることによってstateの変化でuseEffectの無限ループを避けることができます。
③react-useライブラリの useUpdateEffect を使うについて、
import { useUpdateEffect } from "react-use"
//省略
useUpdateEffect(() => {
console.log("useEffect!");
startTimer();
},[count]);
useUpdateEffectをそのまま読み込むだけで初回レンダリング後を実行せずに済みます。
非同期処理を挟みたい
useEffectの副作用関数にそのまま async を当てても使えません。
対処法としては、
①useEffect内で非同期の関数を呼び出す
②useEffect内でasyncをラップした関数を記述する
①useEffect内で非同期の関数を呼び出すについて、
import React, { useState, useEffect, useRef } from "react";
//省略
useEffect(() => {
if(isFirstRen === 0) return;
console.log("useEffect!");
startTimer(); //非同期処理の関数を呼び出す
},[count]);
②useEffect内でasyncをラップした関数を記述するについて、
//省略
useEffect(() => {
if(isFirstRen === 0) return;
console.log("useEffect!");
(async () => {
await hoge;
})();
});
//または
useEffect(() => {
if(isFirstRen === 0) return;
console.log("useEffect!");
const asyncFunc = async () => {
await hoge;
};
asyncFunc();
});
冗長になれば別の関数で書けば良いかと思います。