副作用とライフサイクル(useEffect)
「副作用」とは
- 純粋な部分(レンダリング)
return <div>...</div>でUIを組み立てる部分- 引数(props/state)が同じなら常に同じUIが返るのが理想
- 副作用(Side Effect)
- レンダリング以外の外部に影響する処理
- 例:
- API通信(
fetch) setTimeoutやsetInterval- DOMへの直接アクセス(
document.addEventListener) - ログ出力(Analytics、console.logも広義では副作用)
- API通信(
Reactは「UI = state の関数」にしたいので、UIの組み立てと副作用をはっきり分けたい
⇒ そのための専用の場所が useEffect です。
useEffect の基本形
import { useEffect } from 'react'
useEffect(() => {
// ここに副作用を書く
})ただし、実戦で素の形を使うことはほぼなくて、依存配列(第二引数)をセットで使います:
useEffect(() => {
// 副作用
}, [依存する値])Reactの解釈は、「依存配列の中の値が変わったときに、この関数を実行する」です。
依存配列のパターンは主に3つ:
[](空配列) … 初回マウント時だけ[someState]… その値が変わるたび- 省略 … 毎レンダリングごと(あまり使わない)
初回マウント時だけ実行:useEffect(..., [])
一番よく使うのがこれです。
「コンポーネントが画面に現れたときだけ、1回だけ実行したい」処理用です。
例:初期表示時にログを出す
import { useEffect } from 'react'
export default function App() {
useEffect(() => {
console.log('App がマウントされました')
}, []) // ← 空配列:最初の1回だけ
return <div>こんにちは</div>
}Reactのイメージ:
- 初回レンダリングが終わる
[]のuseEffectの中身を実行- 以後、このコンポーネントが残っている間は二度と実行しない
特定の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が変わるたびに、- 前回のクリーンアップがまず実行される
- その後、新しい副作用が実行される
という順序になります。
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を毎回新しいオブジェクトで作っていると、内容が同じでも「別オブジェクト」と見なされる
⇒ 毎回実行される
この辺はもう少し進んでから、
useCallbackuseMemo
を学ぶときに改めて整理するとスッキリします。
ここでは「依存配列は難所だから、最初のうちはシンプルなパターンから慣れる」くらいで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を定義して実行する形が基本 - 依存配列を書かないと「毎レンダリング後に実行」になり、無限ループの温床になるので注意
コメント