💯

useEffectの挙動をクイズ形式で振り返る

2022/04/16に公開
2

はじめに

普段はスタートアップでBtoB SaaSを作っているtaroと申します!
今回はuseEffectの挙動をしっかりと理解するために、クイズ形式で振り返っていこうと思います。
https://ja.reactjs.org/docs/hooks-effect.html

普段、何気なーく使っていますが、便利なものって便利がゆえに詳しく調べる機会ってあんまりないですよね。
僕自身、最初は動くことを優先した開発の中でReactを学んできたため、こういった基礎中の基礎の学習が結構おろそかになっています。
なので今回は、useEffectの挙動をクイズ形式で1つ1つ振り返っていこうと思います。
ぜひ1問1問考えながら、楽しく読んでいただければ幸いです!

言葉の定義

クイズ中に使用する言葉の定義です!

render

ここでは関数Componentのreturnを実行して、DOMをブラウザに描画することを指します。

副作用編

最初は副作用が実行されるタイミングのクイズです。

Q1: 第2引数の配列に要素を渡した場合

問題:countが出力されるのはいつでしょうか?(該当するものすべて答えよ)

  1. 初回render後
  2. 再render後にcountが変化していた場合
  3. Unmount時

※Mount時、Update時だと、いくつかのphaseが含まれるため、初回render後、再render後としている。

const [count, setCount] = useState(0);

useEffect(() => {
  console.log(count);
}, [count]);
解答
  1. ⭕ 初回render後
  2. ⭕ 再render後にcountが変化していた場合
  3. Unmount時

useEffectの第2引数の配列に要素を渡した場合は、初回render後と再render後にその要素が変化していた場合に、第1引数に渡した副作用が実行されます。

Q2: 第2引数に空配列を渡した場合

問題:'hoge'が出力されるのはいつでしょうか?(該当するものすべて答えよ)

  1. 初回render後
  2. 再render後
  3. Unmount時
useEffect(() => {
  console.log('hoge');
}, []);
解答
  1. ⭕ 初回render後
  2. 再render後
  3. Unmount時

useEffectの第2引数の空配列に要素を渡した場合は、初回render時のみ第1引数のコールバック関数の中身が実行されます。
例えばリアルタイム性が不要なGETのAPI通信等は、初回render後に一度だけ実行すれば良いので、第2引数に空配列を入れてあげて不要なAPI通信を減らしてあげましょう。

Q3: 第2引数に何も渡さない場合

問題:'hoge'が出力されるのはいつでしょうか?(該当するものすべて答えよ)

  1. 初回render後
  2. 再render後
  3. Unmount時
useEffect(() => {
  console.log('hoge');
});
解答
  1. ⭕ 初回render後
  2. ⭕ 再render後
  3. Unmount時

useEffectの第2引数に何も渡さない場合は、初回render後と再render後に第1引数のコールバック関数の中身が実行されます。
これはすべての再render後に実行されるため、もしrenderを最適化する場合は見直しが必要かもしれません。
また実際は初回render後に一度実行するだけでよいのに、空配列を渡しておらずrenderの度に処理が実行されているケースもたまに見かけます。(僕もよくやらかしてました笑)

副作用のクリーンアップ編

次は副作用のクリーンアップのクイズです。

Q4: 第2引数の配列に要素を渡した場合

問題:countが出力されるのはいつでしょうか?(該当するものすべて答えよ)

  1. 初回render後
  2. 再render後にcountが変化していた場合
  3. Unmount時
useEffect(() => {
  return () => {
    console.log(count);
  };
}, [count]);
解答
  1. 初回render後
  2. ⭕ 再render後にcountが変化していた場合
  3. ⭕ Unmount時

useEffectを使ってクリーンアップを実装する場合、第一引数のコールバック関数のreturnの内にクリーンアップを記載します。
useEffectの第2引数の配列に要素を渡した場合は、再render後にその要素が変化していた場合とUnmount時に、クリーンアップが実行されます。

注意するのは、クリーンアップはUnmount時だけでなく再render後にも実行される点です。
ではなぜUnmount時だけでなく、再render後にも実行されるのでしょうか?

なぜ再render後もクリーンアップが実行されるのか?

詳しくは公式ドキュメントではClass Componetで発生していた更新ロジックを書き忘れによるバグを防止するためとしています。

クラスコンポーネントでよく見られた更新ロジック書き忘れによるバグを防止することができます。
https://ja.reactjs.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update

例えば

  • Mount時
    • 購読を開始
  • Unmount時
    • 購読を解除(クリーンアップ)

のような購読処理に、購読先変更の処理を加えたい場合には、

  • Mount時
    • 購読を開始
  • Update時
    • 既存の購読を解除(クリーンアップ)
    • 新しい購読を開始
  • Unmount時
    • 購読を解除(クリーンアップ)

のように、一度既存の購読を解除する必要があります。
こういった状態の変化に対応する際に、Unmount時とUpdate時の両方に同様の処理を書く必要があったのが、useEffectのクリーンアップはUnmount時と更新時の両方で実行されるため処理を1箇所に書くだけで良くなり、Update時のクリーンアップを書き忘れることによるバグが発生しずらくなりました。

少し説明が長くなりましたが、次の問題に進みます。

Q5: 第2引数に空配列を渡した場合

問題:'hoge'が出力されるのはいつでしょうか?(該当するものすべて答えよ)

  1. 初回render後
  2. 再render後
  3. Unmount時
useEffect(() => {
  return () => {
    console.log('hoge');
  };
}, []);
解答
  1. 初回render後
  2. 再render後
  3. ⭕ Unmount時

第2引数に空配列を渡した場合は、Unmount時のみクリーンアップが実行されます。
これはQ2と同様に考えると自然な動きですね。

Q6: 第2引数に何も渡さない場合

問題:'hoge'が出力されるのはいつでしょうか?(該当するものすべて答えよ)

  1. 初回render後
  2. 再render後
  3. Unmount時
useEffect(() => {
  return () => {
    console.log('hoge');
  };
});
解答
  1. 初回render後
  2. ⭕ 再render後
  3. ⭕ Unmount時

第2引数を渡さない場合は、再render後とUnmount時にクリーンアップが実行されます。
これもQ3で述べたことと同様に、不必要にクリーンアップを実行している場合は第2引数を渡して、処理を最適化してあげましょう。

副作用×クリーンアップ編

最後に副作用とクリーンアップが両方ある場合のクイズです。

Q7: 第2引数の配列に要素を渡した場合

問題:count01に変化した場合に、consoleには何が出力されるでしょうか?
consoleに出力される順番と出力されるcountの値が何かをお考えください。

useEffect(() => {
  console.log('副作用', count)
  return () => {
    console.log('クリーンアップ', count);
  };
}, [count]);
解答
クリーンアップ 0
副作用 1

答えは、クリーンアップが先に実行され、またクリーンアップでは変化前の要素の値、副作用では変化後の要素の値を参照します。
ではなぜクリーンアップでは変化前の値を参照するのでしょうか?

なぜクリーンアップでは変化前の値を参照するのか?

これはクリーンアップの目的を考えると自然に理解できます。

クリーンアップはUnmount時と再render後にその前の副作用を解除するために使われ、解除するためには通常、変化前の値を使用します。
例えば、idの変化によって購読先を帰る場合は、以下のような実装になるでしょう。

const [id, setId] = useState(initId)

useEffect(() => {
    // 購読開始
    subscribe(id)
  return () => {
    // 購読解除
    unsubscribe(id)
  };
}, [id]);

この場合、idが12に変化した時にunsubscribe()に渡るべきidは、購読を解除するidである1です。
以上のような用途から、クリーンアップでは変化前の値を参照していることが自然に理解できます。

お世話になったページ

https://ja.reactjs.org/docs/hooks-effect.html

https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

https://www.robinwieruch.de/react-function-component/

https://zenn.dev/yodaka/articles/7c3dca006eba7d

まとめ

7問のクイズを通してuseEffectの挙動を振り返ってみました。
みなさんは何問正解できたでしょうか?
個人としてはなんとなく使っていた部分が、しっかり人に説明できるレベルまで理解が深まりました。
また改めてHooksは使いやすく作られているなーと感じました。(その分、若干見えづらくなっている部分もありますが)
冒頭にも述べましたが、こういった便利なものはなんとなくで使いがちですが、しっかりと使い方や作り手の意図を理解した上で、使いこなしていきたいと思います!

GitHubで編集を提案

Discussion

tagucchtagucch

勉強になる記事をありがとうございました!
普段使っているはずなのに全問正解できなかったので改めて学ぶいい機会となりました。
問題形式なのも面白かったですが、スクロールしすぎて意図せず答えが見えてしまうことがあったため、Zennの記法にあるアコーディオンを使って答えを隠してみるのはどうでしょうか?
ZennのMarkdown記法一覧

:::details 解答
1. ⭕ ……
2. ❌ ……
3. ❌ ……
:::

のような感じになるかと思います!

tarotaro

ありがとうございます!
アコーディオン知りませんでした…!
良さそうなので採用させていただき更新しましたー!