React: フックのルールとよくある落とし穴
最終更新:2026-10-26
1. 学習内容
- フックの2つの鉄則(なぜ条件付きで呼び出せないのか)
- 古いクローズの特定と修正
useEffectで無限ループが発生する5つの理由- useState を使用した非同期更新の落とし穴
- 最もよくある10のバグに対する解決策
2. デバッグの悪夢の物語
(1) 課題:たった1つのuseEffectのデバッグに3時間もかかってしまった
アリスはカウントダウンコンポーネントを作成しました:
▶ サンプル:Hooks Rule デモ
出力:
TEXT
状態:timeLeft。useEffect が副作用を管理。タイマーによる更新
JSX
function Countdown({ targetDate }) {
const [timeLeft, setTimeLeft] = useState(computeTimeLeft())
function computeTimeLeft() {
return Math.max(0, Math.floor((new Date(targetDate) - Date.now()) / 1000))
}
useEffect(() => {
const timer = setInterval(() => {
// ❌ Closure Pitfalls:timeLeft Always the initial value
setTimeLeft(timeLeft - 1)
}, 1000)
return () => clearInterval(timer)
}, []) // I want it to start only when mounted
return <p>Remaining:{timeLeft}s</p>
}
出力:
TEXT
useEffect が副作用を管理
バグ:カウントダウンは常に初期値の-1で停止してしまう。これは、setIntervalのコールバックで取得されるtimeLeftが、最初のレンダリング時の値であるためである(クロージャートラップ)。
(2) 修正
JSX
// ✅ Fix 1:Functional Updates
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(prev => Math.max(0, prev - 1)) // prev Always the latest value
}, 1000)
return () => clearInterval(timer)
}, [])
// ✅ Fix 2:Or put targetDate Add to the dependencies array
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(prev => Math.max(0, prev - 1))
}, 1000)
return () => clearInterval(timer)
}, [targetDate]) // targetDate Restart the timer when a change occurs
レッスン:useEffect で状態を直接読み取って自身を更新してはいけません。関数による更新 setState(prev => prev + 1) を使用してください。
3. フックの2つの鉄則
(1) ルール 1:フックは最上位レベルでのみ呼び出す
ループ、条件文、またはネストされた関数内では、フックを呼び出さないでください:
JSX
function BadComponent({ showExtra }) {
const [name, setName] = useState('') // ✅ Top Floor
// ❌ Rule Violation:Called within a condition Hook
if (showExtra) {
const [extra, setExtra] = useState('') // Error!
useEffect(() => { /* ... */ }, [])
}
// ❌ Rule Violation:Called within a loop Hook
for (let i = 0; i < items.length; i++) {
useEffect(() => { /* ... */ }, []) // Error!
}
// ✅ Correct:Conditional logic in Hook Internal Processing
const [extra, setExtra] = useState('')
useEffect(() => {
if (showExtra) { /* Conditional logic is placed in useEffect Inside */ }
}, [showExtra])
}
| 発信元 | 有効 | 理由 |
|---|---|---|
| トップレベルの関数コンポーネント | ✅ | フックチェーンの順序は、レンダリングのたびに一貫して維持される |
| トップレベルのカスタムフック | ✅ | カスタムフックは、本質的には関数コンポーネントのサブコールチェーンである |
| if / for / while の内部 | ❌ | 条件の変化によりフックの呼び出し回数が一貫性を失い、リンクリストの位置ずれが生じる |
| 汎用ユーティリティ関数 | ❌ | Reactコンポーネントのコンテキスト外;フックのチェーンなし |
| クラスコンポーネントのメソッド | ❌ | クラスコンポーネントにはフックチェーンの仕組みがない |
| useEffect のコールバック内部 | ❌ | コールバックはコンポーネントの最上位レベルにないため、呼び出し順序が予測できない |
(2) ルール 2: フックは関数コンポーネントまたはカスタムフック内でのみ呼び出す
JSX
// ❌ Called from a regular function Hook
function getData() {
const [data, setData] = useState(null) // Error!
}
// ❌ Calling within a class component Hook
class MyClass extends React.Component {
render() {
const [count, setCount] = useState(0) // Error!
}
}
// ✅ Custom Hook Called in
function useCustomHook() {
const [data, setData] = useState(null) // ✅ Correct
}
(3) なぜこの2つのルールがそれほど重要なのでしょうか?
graph TB
A[Component Rendering] --> B[Hook Linked List]
B --> C[Hook 1: useState<br/>Linked List Nodes 1]
B --> D[Hook 2: useEffect<br/>Linked List Nodes 2]
B --> E[Hook 3: useRef<br/>Linked List Nodes 3]
D -->|Conditional rendering causes skipping| F[Linked List Misalignment!]
F --> G[Hook 2 What I read was Hook 3 data]
G --> H[Bug! Data Chaos]
style F fill:#ff4d4f,color:#fff
style H fill:#ff4d4f,color:#fff
Reactは、状態を同期させるためにフックの呼び出し順序に依存しています。レンダリングのたびに呼び出し順序が異なると、状態の同期が崩れてしまいます。
4. 落とし穴 1:古いクロージャーの落とし穴
最もよくある Hooks のバグ。
JSX
function Counter() {
const [count, setCount] = useState(0)
// ❌ Closure Pitfalls
useEffect(() => {
const timer = setInterval(() => {
console.log(count) // Always 0!
setCount(count + 1) // Set permanently to 1
}, 1000)
return () => clearInterval(timer)
}, []) // count is "locked" to first render
// ✅ Fix 1:Functional Updates
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1) // prev Always the latest value
}, 1000)
return () => clearInterval(timer)
}, [])
// ✅ Fix 2:Add Dependencies
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(timer)
}, [count]) // count Restart the timer when a change occurs
}
発生時:useEffect、useCallback、またはuseMemoで、dependencies配列に宣言されていない値が読み込まれました。
| クロージャーの落とし穴となるシナリオ | 症状 | 解決策 |
|---|---|---|
| setInterval の状態の読み取り | タイマーの値がまったく変わらない | 関数型更新 setState(prev => ...) |
useCallback での状態の読み取り |
コールバック内で古い値を使用 | 依存関係配列への状態の追加、または ref の使用 |
| イベントハンドラの実行が遅延している | 非同期コールバック内の state の値が古くなっている |
useRef を使用して最新の値を保存してください |
| useEffect の依存関係が不足しています | エフェクト内で使用されている変数が更新されていません | 依存関係配列を完成させてください |
| 非同期リクエスト後にレースコンディションが発生する | 高速切り替え時に、古いリクエストが新しい結果を上書きしてしまう | ignore フラグまたは AbortController を使用する |
トラブルシューティングのヒント: コールバック内で読み込まれる変数はすべて、dependencies 配列で宣言されている必要があります。あるいは、関数型の更新を使用してください。
5. 落とし穴 2:useEffect の無限ループ
(1) 理由 1:依存関係の配列がない
JSX
// ❌ Infinite Loop!
useEffect(() => {
fetch('/api/data').then(setData)
}) // No array dependencies → Executed on every render → setData Trigger Rendering → Infinite Loop
// ✅ Empty array of dependencies
useEffect(() => {
fetch('/api/data').then(setData)
}, [])
(2) 理由 2:依存関係配列内の参照型
JSX
function App() {
const [count, setCount] = useState(0)
// ❌ Infinite Loop!
useEffect(() => {
console.log('useEffect Execute')
}, [{ count }]) // Create a new object on every render {} → Differences in Citation → Infinite Execution
// ✅ Correct:Using Primitive Types
useEffect(() => {
console.log('useEffect Execute')
}, [count]) // Basic Types,If the value remains unchanged, the action is not performed.
}
(3) 理由 3:useEffect 内の依存関係を変更する
JSX
const [items, setItems] = useState([1, 2, 3])
// ❌ Infinite Loop!
useEffect(() => {
setItems([...items, 4]) // Edit items → Render Again → useEffect Execute again...
}, [items])
// ✅ Correct:Why is this change necessary? items?Generally, it is not necessary.
// If it's initialization,use useRef Flag indicating whether initialization has been completed
const initialized = useRef(false)
useEffect(() => {
if (!initialized.current) {
setItems([...items, 4])
initialized.current = true
}
}, [items])
(4) 理由 4:関数としての依存関係
| 無限ループの原因 | 誤ったコード | 正しいコード |
|---|---|---|
| 配列への依存関係なし | useEffect(fn) |
useEffect(fn, []) |
| 参照型 | useEffect(fn, [{ count }]) |
useEffect(fn, [count]) |
effect で依存関係を変更する |
useEffect(() => setItems(...), [items]) |
useRef で初期化をマークする |
| 依存関係としての関数 | useEffect(fn, [handleClick]) |
useCallback 安定した参照 |
| 依存関係としてのオブジェクト | useEffect(fn, [config]) |
プリミティブ型の値を抽出するか、useMemo を使用する |
JSX
// ❌ Each render creates a new function reference
function App() {
const handleClick = () => { /* ... */ }
useEffect(() => {
fetch('/api').then(setData)
}, [handleClick]) // handleClick A new reference every time → Infinite Execution
// ✅ use useCallback Stable Citation
const handleClick = useCallback(() => { /* ... */ }, [])
useEffect(() => {
fetch('/api').then(setData)
}, [handleClick]) // handleClick Citation unchanged,Will not be executed again
}
6. 落とし穴 3:useState による非同期更新
JSX
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
// You thought it would +3,The result was only +1
setCount(count + 1)
setCount(count + 1)
setCount(count + 1) // All three times count + 1, but count = 0
console.log(count) // Output 0,No 3
}
// ✅ Functional Updates
function handleCorrectClick() {
setCount(prev => prev + 1) // prev = 0 → 1
setCount(prev => prev + 1) // prev = 1 → 2
setCount(prev => prev + 1) // prev = 2 → 3
// count Or 0(The rendered values for this pass),But the next time it's rendered, it will be 3
}
}
重要な概念:useStateへの更新は非同期かつバッチ処理されます。1回のイベント処理サイクル中に、複数のsetCountインスタンスが1つのレンダリングにまとめられます。関数型更新は、すべての更新が最新の値に基づいて行われることを保証するために使用されます。
7. 落とし穴 4:useEffect 内で async を使用すること
JSX
// ❌ useEffect Cannot be transferred directly async Function
useEffect(async () => {
const data = await fetch('/api/data')
setData(await data.json())
}, [])
// Error!async returns Promise, useEffectt Expected return value of the cleanup function(or undefined)
// ✅ Correct:Defined internally async Function
useEffect(() => {
async function fetchData() {
const res = await fetch('/api/data')
const data = await res.json()
setData(data)
}
fetchData() // Internal Call async Function
}, [])
8. 完全な例:10の一般的なバグ診断表
JSX
// ============================================
// Example:10 A common Hooks Bug + Remediation Plan
// ============================================
// Bug 1:Conditions Hook
function Bug1({ show }) {
// if (show) const [x] = useState(0) // ❌
const [x] = useState(0) // ✅ Move to the top
}
// Bug 2:Closure Pitfalls
function Bug2() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// setCount(count + 1) // ❌ Closure
setCount(prev => prev + 1) // ✅
}, 1000)
return () => clearInterval(id)
}, [])
}
// Bug 3:Infinite Loop(Dependency on Reference Types)
function Bug3() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api').then(res => res.json()).then(setData)
}, []) // ✅ Empty array
// }, [data]) // ❌ data The change triggered another one useEffect
}
// Bug 4:Forgot to clean up
function Bug4() {
useEffect(() => {
const id = setInterval(() => console.log('tick'), 1000)
return () => clearInterval(id) // ✅ Must be cleaned up
}, [])
}
// Bug 5:Edit directly state
function Bug5() {
const [user, setUser] = useState({ name: 'Alice', age: 28 })
function updateAge() {
// user.age = 29 // ❌ Edit directly
setUser({ ...user, age: 29 }) // ✅ Create a New Object
}
}
// Bug 6:Not needed useMemo
function Bug6({ count }) {
// const doubled = useMemo(() => count * 2, [count]) // ❌ Unnecessary
const doubled = count * 2 // ✅ Simple calculations are not required useMemo
}
// Bug 7:effect Read from props But do not declare dependencies
function Bug7({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch('/api/users/' + userId).then(res => res.json()).then(setUser)
}, [userId]) // ✅ userId Dependencies must be added
// }, []) // ❌ userId Changes will not trigger a new request
}
// Bug 8:useMemo Return Function
function Bug8() {
// const fn = useMemo(() => { return () => console.log('hi') }, []) // ❌ Should use useCallback
const fn = useCallback(() => console.log('hi'), []) // ✅
}
// Bug 9:in render Side effects caused by the drug
function Bug9() {
const [data, setData] = useState(null)
// fetch('/api/data').then(setData) // ❌ A request is made for every render
useEffect(() => { fetch('/api/data').then(res => res.json()).then(setData) }, []) // ✅
}
// Bug 10:State dependency not fully declared
function Bug10({ items }) {
const [filter, setFilter] = useState('')
// const filtered = items.filter(i => i.includes(filter)) // ✅ in render Calculate directly in the middle
const filtered = useMemo(() => items.filter(i => i.includes(filter)), [items, filter])
// useMemo The correct dependencies must be in place.
}
9. デバッグのヒント
(1) React DevTools プロファイラー
JSX
// in React DevTools 's Profiler In the tab:
// 1. Click the Record button
// 2. Triggering Actions in the App
// 3. Stop Recording
// 4. View the flame diagram:Each column is a component,Rendering Time by Height
// I see components that shouldn't be re-rendered being rendered → Inspection useCallback / React.memo
(2) なぜレンダリングしたのか
BASH
npm install @welldone-software/why-did-you-render
JSX
// src/wdyr.js
import React from 'react'
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackAllPureComponents: true,
})
}
// The console will print:Why did this component re-render??Because which one Props It's changed?
(3) useWhyDidYouUpdate を使ったデバッグ
▶ 試してみよう
出力:
TEXT
状態:timeLeft(setter:setTimeLeft)。useEffect が副作用を管理。タイマーベースの更新
▶ サンプル:カスタムフックを使用してクロージャの落とし穴を回避する — useStableCallback
▶ 実行例を展開(70 行)
出力:
TEXT
❌ 条件文の中で Hook を呼ぶとバグが発生する。✅ 常にトップレベルで Hook を呼ぶ。ルール:レンダリングのたびに Hook の呼び出し順序を一致させること。
出力:
TEXT
... ('Current Count:', count)
▶ サンプル:useState を使用した一括更新における落とし穴と対処法
▶ 試してみよう出力:
TEXT
レンダリング:Shopping Cart + Total: ${total}
出力:
TEXT
表示:"Shopping Cart"。状態:items(setter:setItems)、total(setter:setTotal)。ボタン:addItemFixed({ id: 3, name: 'Notebook', price: 12 })}> Add Notebook、Add Book + Pen。リスト:{item.name} - ${item.price}
▶ サンプル:useEffect を使用した非同期リクエストにおけるレースコンディション
JSX
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
let ignore = false
setLoading(true)
async function fetchUser() {
try {
const res = await fetch(`/api/users/${userId}`)
const data = await res.json()
if (!ignore) {
setUser(data)
setLoading(false)
}
} catch (err) {
if (!ignore) setLoading(false)
}
}
fetchUser()
return () => { ignore = true }
}, [userId])
if (loading) return <p>Loading...</p>
if (!user) return <p>No user found</p>
return (
<div>
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
</div>
)
}
出力:
TEXT
try { await fetchData(); } catch (err) { setError(err.message); } → "Failed to load data" を表示。エラーは処理済みで、アプリは動作を続ける。
出力:
TEXT
表示:"Loading..."。状態:user(setter:setUser)、loading(setter:setLoading)。useEffect が副作用を管理。非同期データ取得/読み込み状態
▶ サンプル:状態の不整合を避けるために、複数の useState 呼び出しの代わりに useReducer を使用する
▶ 試してみよう
出力:
TEXT
レンダリング:Count: {state.count} + History: {state.history.join(', ')}
❓ よくある質問
Q
setState を呼び出した後、ページが更新されないことがあるのはなぜですか?A 考えられる理由:①
state オブジェクトを直接変更した(新しい参照を作成せずに); ② 状態の値が変わらなかった(ReactはObject.isを使って値を比較するため、値が同一の場合はレンダリングがトリガーされない); ③ useEffectのdependencies配列に、更新された変数を宣言し忘れた; ④ 非同期コールバック内で古いクロージャの値を読み取ってしまった。Q
useEffect で、dependencies 配列を空に設定する場合と、単に空のままにしておく場合の違いは何ですか?A dependencies 配列を空のままにする場合
useEffect(fn) → レンダリングのたびに実行されます(マウント時および更新のたびに); 空のdependencies配列 useEffect(fn, []) → マウント時に1回のみ実行されます;空でないdependencies useEffect(fn, [a, b]) → マウント時に実行されるほか、aまたはbが変更された際にも実行されます。最も一般的なバグの原因:空の配列を指定しながら、エフェクト内でstateやpropsを使用してしまうこと(クロージャートラップ)。📖 まとめ
- 2つの鉄則:フックは最上位レベルでのみ呼び出し、また関数コンポーネントまたはカスタムフック内でのみ呼び出すこと
- クロージャの注意点:
useEffectまたはuseCallback内でアクセスされる変数は、dependencies 配列で宣言するか、関数を使用して更新する必要があります。 - 無限ループ:依存関係配列が正しいか、参照型が安定しているか、およびエフェクト内で依存関係が変更されていないかを確認する
- 関数型更新:
setCount(prev => prev + 1)クロージャーに関する落とし穴と一括更新の問題を修正 eslint-plugin-react-hooksをインストールしてください。手動で無効にしないでください。- React DevToolsのプロファイラーを使用して、パフォーマンスのボトルネックを特定する