React useEffect
useEffect 是 React 用來處理「副作用」(side effects)的 Hook。副作用是指那些會影響元件外部或無法在渲染過程中完成的操作,例如:
- 資料獲取(API 呼叫)
- 訂閱(WebSocket、事件監聽)
- 手動操作 DOM
- 設定計時器
- 讀寫 localStorage
基本語法
import { useEffect } from 'react'
useEffect(() => {
// 副作用程式碼
}, [dependencies])
- 第一個參數:一個函式,包含要執行的副作用程式碼
- 第二個參數:依賴陣列(dependency array),決定何時重新執行副作用
依賴陣列的三種情況
1. 沒有依賴陣列 - 每次渲染都執行
useEffect(() => {
console.log('每次渲染後都會執行')
})
這種寫法通常是錯誤的,因為它會導致無限迴圈或效能問題。
2. 空陣列 [] - 只在首次渲染後執行
useEffect(() => {
console.log('只在元件掛載時執行一次')
}, [])
這相當於以前 Class Component 的 componentDidMount。
3. 有依賴項目 - 依賴變動時執行
useEffect(() => {
console.log(`count 變成了 ${count}`)
}, [count])
當 count 的值改變時,這個 effect 會重新執行。
實際範例
範例 1:資料獲取
import { useState, useEffect } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
// 重置狀態
setLoading(true)
setError(null)
// 獲取使用者資料
fetch(`https://api.example.com/users/${userId}`)
.then((response) => {
if (!response.ok) {
throw new Error('無法載入使用者資料')
}
return response.json()
})
.then((data) => {
setUser(data)
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [userId]) // 當 userId 改變時重新獲取
if (loading) return <p>載入中...</p>
if (error) return <p>錯誤:{error}</p>
if (!user) return null
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
範例 2:更新文件標題
import { useState, useEffect } from 'react'
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
// 更新瀏覽器標題
document.title = `你點擊了 ${count} 次`
}, [count])
return (
<div>
<p>你點擊了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>點我</button>
</div>
)
}
清理函式(Cleanup)
有些副作用需要「清理」,例如取消訂閱、清除計時器、移除事件監聽。你可以在 effect 函式中返回一個清理函式:
useEffect(() => {
// 設定副作用
return () => {
// 清理副作用
}
}, [dependencies])
清理函式會在:
- 元件卸載(unmount)時執行
- 下一次 effect 執行之前執行(當依賴改變時)
範例:計時器
import { useState, useEffect } from 'react'
function Timer() {
const [seconds, setSeconds] = useState(0)
useEffect(() => {
// 設定計時器
const intervalId = setInterval(() => {
setSeconds((prev) => prev + 1)
}, 1000)
// 清理:清除計時器
return () => {
clearInterval(intervalId)
}
}, []) // 空陣列:只在掛載時設定,卸載時清理
return <p>已經過 {seconds} 秒</p>
}
範例:事件監聽
import { useState, useEffect } from 'react'
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
})
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
// 新增事件監聽
window.addEventListener('resize', handleResize)
// 清理:移除事件監聽
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return (
<p>
視窗大小:{size.width} x {size.height}
</p>
)
}
常見錯誤與解決方案
錯誤 1:忘記清理訂閱
// ❌ 錯誤:沒有清理事件監聽
useEffect(() => {
window.addEventListener('scroll', handleScroll)
}, [])
// ✅ 正確:記得清理
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
錯誤 2:依賴陣列遺漏依賴
// ❌ 錯誤:effect 使用了 userId 但沒有放在依賴中
useEffect(() => {
fetchUser(userId)
}, []) // userId 改變時不會重新獲取
// ✅ 正確:將所有使用到的值放入依賴
useEffect(() => {
fetchUser(userId)
}, [userId])
錯誤 3:物件/陣列作為依賴
// ❌ 問題:每次渲染 options 都是新物件,導致 effect 不斷執行
function Component({ id }) {
const options = { id, verbose: true }
useEffect(() => {
fetchData(options)
}, [options]) // 每次都是新物件!
}
// ✅ 解法 1:將物件移到 effect 內部
useEffect(() => {
const options = { id, verbose: true }
fetchData(options)
}, [id])
// ✅ 解法 2:使用 useMemo
const options = useMemo(() => ({ id, verbose: true }), [id])
useEffect(() => {
fetchData(options)
}, [options])
處理非同步操作
useEffect 的函式不能是 async,但你可以在內部定義 async 函式:
useEffect(() => {
// ❌ 錯誤:useEffect 不能直接用 async
// async () => { ... }
// ✅ 正確:在內部定義 async 函式
async function fetchData() {
try {
const response = await fetch('/api/data')
const data = await response.json()
setData(data)
} catch (error) {
setError(error)
}
}
fetchData()
}, [])
處理競態條件(Race Condition)
當快速連續請求時,可能會出現舊的請求覆蓋新的結果:
useEffect(() => {
let cancelled = false
async function fetchData() {
const response = await fetch(`/api/users/${userId}`)
const data = await response.json()
// 只有在沒有被取消時才更新狀態
if (!cancelled) {
setUser(data)
}
}
fetchData()
// 清理:標記這次請求已被取消
return () => {
cancelled = true
}
}, [userId])
useEffect vs 事件處理
不是所有的邏輯都應該放在 useEffect 中。一個簡單的判斷原則:
- 事件處理:由使用者操作觸發的動作(點擊、輸入、提交)
- useEffect:需要與外部系統同步的操作
function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
// ✅ 事件處理:使用者提交搜尋
function handleSubmit(e) {
e.preventDefault()
search(query)
}
// ✅ useEffect:頁面載入時獲取熱門搜尋
useEffect(() => {
fetchPopularSearches().then(setResults)
}, [])
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="submit">搜尋</button>
</form>
)
}