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。