Effect 完全指南:怎么用、何时用、何时不用
从忘掉生命周期到避免常见误用——Dan Abramov 的 useEffect 深度理解
来源:overreacted.io/a-complete-guide-to-useeffect/
核心观点:忘掉生命周期,用同步思维理解 useEffect
❌ 生命周期思维:componentDidMount → useEffect(fn, [])
✅ 同步思维:effect 是跟外部系统同步的机制,deps 是同步触发条件
1. 每次渲染都是独立快照(深度版)
// 第一次渲染:count = 0
// 第二次渲染:count = 1
// 第三次渲染:count = 2
// 每个渲染的 count 是独立的常量,不会变
关键: count 不是“数据绑定”,不是“响应式变量”。它就是一个数字。React 只是每次调用你的组件函数时传入不同的值。
2. 事件处理器捕获的是“当时的”值
function Counter() {
const [count, setCount] = useState(0)
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count) // 捕获的是点击时的 count
}, 3000)
}
return <button onClick={handleAlertClick}>Click</button>
}
// 点击时 count=3,3 秒后 alert 显示 3
// 即使这期间 count 变成了 5,alert 仍然显示 3
原理: 每次渲染返回一个新的 handleAlertClick 函数,每个函数闭包捕获自己的 count。事件处理器“属于”某次特定的渲染。
3. Effect 也是独立的快照
useEffect(() => {
document.title = `You clicked ${count} times`
})
// 不是 count 在 effect 里"自动更新"
// 而是每次渲染都创建一个新的 effect 函数
// 每个 effect 函数闭包捕获自己的 count
概念上,effect 是渲染结果的一部分。 每次渲染都会产生一个新的 effect,React 在浏览器绘制后执行它。
4. 为什么 useEffect(fn, []) 不能完全替代 componentDidMount
// class 组件
componentDidMount() {
// this.props 是最新的
console.log(this.props.user) // ✅ 最新值
}
// 函数组件
useEffect(() => {
// count 捕获的是第一次渲染的值
console.log(count) // 🔴 永远是 0
}, [])
[] 意味着“这个 effect 不依赖任何渲染值,所以只需要同步一次”。** 但 effect 里的闭包仍然捕获第一次渲染的 props/state。
5. 每个 effect 属于某次渲染
第一次渲染:
count = 0
effect = () => { document.title = 'You clicked 0 times' }
第二次渲染:
count = 1
effect = () => { document.title = 'You clicked 1 times' }
第三次渲染:
count = 2
effect = () => { document.title = 'You clicked 2 times' }
React 按顺序记住这些 effect,浏览器绘制后依次执行。
6. 依赖数组的真相
useEffect(fn) → 每次渲染都执行(没有同步条件)
useEffect(fn, []) → 只在首次渲染后执行(没有依赖,所以只同步一次)
useEffect(fn, [a, b]) → a 或 b 变化后执行(重新同步)
依赖数组不是“执行时机”,而是“同步条件”。 它告诉 React:“这些值变了,需要重新同步。”
7. 无限循环的原因
// 🔴 每次渲染都创建新的对象,deps 永远"变了"
useEffect(() => {
fetchData(options) // options 是对象,引用每次不同
}, [options]) // → 无限循环
// ✅ 用 useMemo 稳定引用
const options = useMemo(() => ({ query }), [query])
useEffect(() => {
fetchData(options)
}, [options])
无限循环 = deps 里有每次渲染都变的值。 解决方法是稳定引用(useMemo/useCallback)或调整 effect 结构。
8. 函数作为依赖
// ✅ 最好把函数移到 effect 内部
useEffect(() => {
function fetchData() { /* ... */ }
fetchData()
}, [userId]) // 只依赖 userId
// 或者移到组件外部(如果不需要 props/state)
function fetchUserData(userId) { /* ... */ }
useEffect(() => {
fetchUserData(userId)
}, [userId])
函数也是渲染值,也会捕获闭包。 如果函数在 effect 里用到了 props/state,它必须在 deps 里。
核心一句话
useEffect 不是 componentDidMount 的替代品。它是一个同步机制:effect 函数是渲染结果的一部分,每次渲染产生新的 effect,闭包捕获当时的值。用同步思维(“什么变了需要重新同步”)而不是生命周期思维(“什么时候执行”)来使用它。
来源:react.dev/learn/you-might-not-need-an-effect
核心观点:很多场景不需要 Effect,直接在渲染时计算
useEffect 是 React 的逃生舱口(escape hatch),用于同步外部系统
如果只是基于 props/state 更新 UI → 不需要 effect
如果只是处理用户事件 → 不需要 effect
1. 不要用 Effect 来计算派生状态
// 🔴 冗余状态 + 不必要的 effect
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName)
}, [firstName, lastName])
// ✅ 直接在渲染时计算
const fullName = firstName + ' ' + lastName
原则:如果一个值可以从现有的 props 或 state 计算出来,不要放进 state,直接计算。
2. 不要用 Effect 来缓存计算结果
// 🔴 用 state + effect 缓存
const [visibleTodos, setVisibleTodos] = useState([])
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter))
}, [todos, filter])
// ✅ 直接计算(如果计算不慢)
const visibleTodos = getFilteredTodos(todos, filter)
// ✅ 如果计算很慢,用 useMemo 缓存
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
)
3. 不要用 Effect 来重置 state
// 🔴 用 effect 在 prop 变化时重置 state
useEffect(() => {
setComment('')
}, [userId])
// ✅ 用 key 让 React 自动重置
<Profile userId={userId} key={userId} />
key 告诉 React:不同的 key 值 = 不同的组件实例,state 自动重置。**
4. 不要用 Effect 来处理用户事件
// 🔴 用 effect 处理购买事件
useEffect(() => {
if (isBuyClicked) {
fetch('/api/buy', { method: 'POST' })
setThankYouMessage(true)
}
}, [isBuyClicked])
// ✅ 直接在事件处理器里处理
function handleBuy() {
fetch('/api/buy', { method: 'POST' })
setThankYouMessage(true)
}
Effect 不知道“是什么触发了更新”。事件处理器知道。
5. 不要用 Effect 来同步 state 和 props
// 🔴 用 effect 把 prop 同步到 state
const [selectedItem, setSelectedItem] = useState(null)
useEffect(() => {
setSelectedItem(fetchedItem)
}, [fetchedItem])
// ✅ 直接用 prop(如果不需要额外 state)
const selectedItem = fetchedItem
6. Effect 的正确使用场景
✅ 同步外部系统(jQuery widget、浏览器 API、WebSocket)
✅ 数据请求(fetch)
✅ 订阅(addEventListener、document.title)
✅ 定时器(setInterval、setTimeout)
✅ 不由你控制的第三方库集成
核心一句话
Effect 是逃生舱口,不是日常工具。如果你只是在做“根据 props/state 计算 UI”,直接在渲染时做。只有在同步外部系统时才用 Effect。