🍊

useEffect の内側を理解する

とあるプロジェクトのエンジニア教育の一環で useEffect の内部構造について解説する機会があってこの度、言語化いたしました。

By jiyuujin at

#React
#TypeScript
useEffect の内側を理解するをはてなブックマークに追加

useEffect

今回は useEffect を中心に見ていく。結論を言うと mountEffectupdateEffect を見れば良い。その理由こそ useEffect がコンポーネントの初期レンダリングを始め、書き手の自由に応じたレンダリングの制御も可能であるため。

実際に元の hook 同様のコールバック関数と依存関係の配列を受け入れる。第 2 引数である依存関係の配列に何も渡されないと、コンポーネントの初期レンダリング時に実行される。

主に下記ケースで useEffect を使うことが多い。

  • 手動で行う DOM 変更関連の処理
  • サーバ API からのデータフェッチの処理
  • イベントリスナーによるサブスクリプションの処理

カウンタアップの例

具体的にカウンタアップとその DOM 更新を例にとる。

初期レンダリングで DOM を更新する。

  • 変数 count を定義する
  • コールバック関数内で DOM を更新する
import { useEffect, useState } from 'react'

const Component = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `You clicked ${count} times`
  })
}

するとコンポーネントは count に初期値が入っていることを確認できる。

# document.getElementsById('title')
You clicked 0 times

続いて依存関係の配列に count を渡してみよう。

  • 変数 count を定義する
  • コールバック関数内で DOM を更新する
  • 第 2 引数に count を設定する
import { useEffect, useState } from 'react'

const Component = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `You clicked ${count} times`
  }, [count])
}

するとコンポーネントは count が変更される度に DOM 更新が実行されるはずです。

# document.getElementsById('title')
You clicked 1 times

それを確認するためカウンタアップ用に適当なボタンを作成してみよう。

するとコンポーネントは count が変更される度に DOM 更新が実行される。

import { useEffect, useState } from 'react'

const Component = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `You clicked ${count} times`
  }, [count])

  return <button onClick={() => setCount(count + 1)}></button>
}

さらに useEffect の内側を理解する

  • 初期レンダリング
  • コールバック関数の第 2 引数である依存関係配列に何らかの値が入った場合の更新

これらのタイミングに同じバッチでレンダリングするための更新キューを実行しています。

実際に何らかの更新する際は enqueueUpdate を実行しています。

function enqueueUpdate(fiber, update) {
  var updateQueue = fiber.updateQueue

  var sharedQueue = updateQueue.shared
  var pending = sharedQueue.pending

  if (pending === null) {
    update.next = update
  } else {
    update.next = pending.next
    pending.next = update
  }

  sharedQueue.pending = update
}

関数を fiber と紐付けることで、各コンポーネントの更新を区別できるようにしています。

無限ループに注意する

しばしば出会す無限ループ。結論を言うと第 2 引数をきちんと追いきれていない可能性があります。

具体的にカウンタアップをコールバック関数内で済ませてしまうケースを例にとります。

import { useEffect, useState } from 'react'

const Component = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    setCount((count) => count + 1)
  }, [count])
}

count の値が変わって、コールバック関数でその都度更新されています。

これこそ無限ループとなってしまう原因です。

これ以外にも非同期通信を処理した場合など useEffect の使いどころを考慮する必要があります。

  • 非同期通信を処理した場合
  • Hooks 用に公開された ESLint プラグインreact-hooks/exhaustive-deps をチェックした場合

他にも注意するべきことは存在するけど、今回はこの辺で。

その他

ひと昔前に書いていたクラスコンポーネントで useEffect を例えると componentDidMountcomponentDidUpdate の組み合わせと説くことができます。

useEffect の内側を理解するをはてなブックマークに追加