[React講座] 副作用とライフサイクル(useEffect)

当ページのリンクには広告が含まれています。
目次

副作用とライフサイクル(useEffect)

「副作用」とは

  • 純粋な部分(レンダリング)
    • return <div>...</div> でUIを組み立てる部分
    • 引数(props/state)が同じなら常に同じUIが返るのが理想
  • 副作用(Side Effect)
    • レンダリング以外の外部に影響する処理
    • 例:
      • API通信(fetch
      • setTimeoutsetInterval
      • DOMへの直接アクセス(document.addEventListener
      • ログ出力(Analytics、console.logも広義では副作用)

Reactは「UI = state の関数」にしたいので、UIの組み立てと副作用をはっきり分けたい
⇒ そのための専用の場所が useEffect です。

useEffect の基本形

import { useEffect } from 'react'

useEffect(() => {
  // ここに副作用を書く
})

ただし、実戦で素の形を使うことはほぼなくて、依存配列(第二引数)をセットで使います:

useEffect(() => {
  // 副作用
}, [依存する値])

Reactの解釈は、「依存配列の中の値が変わったときに、この関数を実行する」です。

依存配列のパターンは主に3つ:

  1. [](空配列) … 初回マウント時だけ
  2. [someState] … その値が変わるたび
  3. 省略 … 毎レンダリングごと(あまり使わない)

初回マウント時だけ実行:useEffect(..., [])

一番よく使うのがこれです。
「コンポーネントが画面に現れたときだけ、1回だけ実行したい」処理用です。

例:初期表示時にログを出す

import { useEffect } from 'react'

export default function App() {
  useEffect(() => {
    console.log('App がマウントされました')
  }, []) // ← 空配列:最初の1回だけ

  return <div>こんにちは</div>
}

Reactのイメージ:

  1. 初回レンダリングが終わる
  2. [] のuseEffectの中身を実行
  3. 以後、このコンポーネントが残っている間は二度と実行しない

特定のstate/propsが変わったときだけ実行

count が変わったら、そのたびにログを出す例です。

import { useState, useEffect } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('カウントが変わりました:', count)
  }, [count]) // ← count が変わるたびに実行

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </div>
  )
}

依存配列 [count] の意味:

  • 初回マウント時にも1回実行
  • その後、count が 前回から変わったときだけ 実行

複数指定も可能です:

useEffect(() => {
  // keyword または page が変わったときに検索実行
}, [keyword, page])

依存配列を省略した場合(基本は非推奨)

useEffect(() => {
  console.log('毎回レンダリングのたびに実行される...')
})
  • 依存配列を渡さないと、「すべてのレンダリング後に毎回実行」になります。
  • 重い処理やsetStateをここでやると簡単に無限ループ・パフォーマンス悪化

慣れるまでは、「useEffectを書いたら必ず依存配列も書く」ようにしておくと安全です。

クリーンアップ(後始末)のある useEffect

タイマーやイベントリスナーのような「登録したら、いつか解除しないといけないもの」は、
useEffect の中で「登録」と「解除」をセットで書きます。

useEffect の返り値に関数を書くと、それがクリーンアップ関数として扱われます。

useEffect(() => {
  console.log('タイマー登録')

  const id = setInterval(() => {
    console.log('1秒ごとに実行')
  }, 1000)

  return () => {
    console.log('タイマー解除')
    clearInterval(id)
  }
}, [])

動き:

  • マウント時:
    • setInterval でタイマー登録
  • アンマウント時(コンポーネントが画面から消えるとき):
    • return () => { ... } が実行され、clearInterval される

依存配列つきの場合:

useEffect(() => {
  console.log('何か設定を適用')

  return () => {
    console.log('前回の設定を解除')
  }
}, [someValue])
  • someValue が変わるたびに、
    1. 前回のクリーンアップがまず実行される
    2. その後、新しい副作用が実行される

という順序になります。

useEffect での API 通信の基本パターン

Reactで「初回表示時にデータを取ってくる」ときの典型例です。

import { useState, useEffect } from 'react'

export default function PostList() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    let isCancelled = false

    async function fetchPosts() {
      try {
        setLoading(true)
        setError(null)

        const res = await fetch('https://jsonplaceholder.typicode.com/posts')
        if (!res.ok) {
          throw new Error('ネットワークエラー')
        }
        const data = await res.json()

        if (!isCancelled) {
          setPosts(data.slice(0, 5)) // 5件だけに絞る例
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message)
        }
      } finally {
        if (!isCancelled) {
          setLoading(false)
        }
      }
    }

    fetchPosts()

    return () => {
      // コンポーネントがアンマウントされたらフラグを立てる
      isCancelled = true
    }
  }, []) // 初回マウント時だけ

  if (loading) return <p>読み込み中...</p>
  if (error) return <p style={{ color: 'red' }}>エラー: {error}</p>

  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  )
}

ポイントいくつか:

  • useEffect 内で async を直接付けない方が良いので、中に async function を定義して呼び出す形がよく使われます。
  • 通信状態用に loading, error などのstateを用意して、UIに反映させると「実務っぽい」構成になる。
  • アンマウント後にstate更新しないよう、isCancelled フラグを用意するパターンはよく見る(本格的には AbortController を使う方法もある)。

よくあるハマりポイント

useEffect内で setState ⇒ 無限ループ

useEffect(() => {
  setCount(count + 1)
}, []) // ← こう書けばOK(初回だけ)

// でも依存配列を書かないと…
useEffect(() => {
  setCount(count + 1)
}) // ← 毎レンダリングで setState ⇒ 永久ループ

対策:

  • 状態を変える処理を書くときは、依存配列に本当に必要なものだけ入れる。
  • そもそも「レンダリングのたびに更新」が必要なケースはかなりレア。

依存配列に入れ忘れ ⇒ 値が古いまま

useEffect(() => {
  console.log('keyword:', keyword)
  // 依存配列を [] にしてしまうと、
}, []) // ← 初回レンダリング時の keyword しか使えない

依存しているstate / propsは、基本的に全部依存配列に入れるのが正しいです。
ESLint(eslint-plugin-react-hooks)を使うと、これを自動でチェックしてくれます。

「関数」「オブジェクト」「配列」を依存に入れたら毎回変わる問題

useEffect(() => {
  // ...
}, [options])
  • options を毎回新しいオブジェクトで作っていると、内容が同じでも「別オブジェクト」と見なされる
    ⇒ 毎回実行される

この辺はもう少し進んでから、

  • useCallback
  • useMemo

を学ぶときに改めて整理するとスッキリします。
ここでは「依存配列は難所だから、最初のうちはシンプルなパターンから慣れる」くらいでOKです。

ライフサイクルのイメージ

昔のクラスベースのReactでは:

  • componentDidMount(初回マウント後)
  • componentDidUpdate(更新後)
  • componentWillUnmount(アンマウント前)

などのライフサイクルメソッドがありました。

useEffect はざっくり言うと:

  • useEffect(fn, [])
    • componentDidMount + componentWillUnmount の組み合わせ
  • useEffect(fn, [deps])
    • componentDidMount + componentDidUpdate(depsが変わったとき) + componentWillUnmount 的な動き

とイメージしておくと、理解しやすいです。

まとめ

  • 副作用 = レンダリング(UI計算)以外の処理(API呼び出し、タイマー、イベント登録など)
  • useEffect(() => { ... }, [])
    • 初回マウント時だけ実行
  • useEffect(() => { ... }, [deps])
    • 指定した deps が変わるたびに実行
  • クリーンアップ(return () => { ... })で、タイマーやイベントリスナーを解除
  • API呼び出しは useEffect 内で async function を定義して実行する形が基本
  • 依存配列を書かないと「毎レンダリング後に実行」になり、無限ループの温床になるので注意

<<前へ(フォームとユーザー入力)

>>次へ(API通信とデータ取得)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次