Dan Abramov 那篇《A Complete Guide to useEffect》很长,但核心就一句话:别再用生命周期的思维想 useEffect,它的本质是"同步"。
下面是我提炼的几个关键认知,搞懂这些,useEffect 的坑基本踩不到。
一、每次渲染都是一张"快照"
函数组件每次渲染,props、state、事件处理函数、effect,全都是那一次渲染的快照。count 不是一个随时间变化的变量,它就是一个常量。
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
alert(count); // 永远是点击时的值,不是最新值
}, 3000);
}
}
点击时 count 是 3,哪怕 3 秒内变成了 5,弹出来的还是 3。因为这个函数"拍"下来的就是那一刻的 count。
useEffect 也一样。每次渲染的 effect 函数,看到的都是当次渲染的 props 和 state。
二、依赖数组是"承诺",不是"优化"
很多人把 [] 当成"只运行一次"的开关。错了。 依赖数组是你对 React 的承诺:"我这个 effect 只用到了这些变量。"
如果 effect 里用了 count,但依赖写了 [],你就是在对 React 撒谎。后果是 effect 永远看到初始值,产生闭包陷阱。
规则很简单:effect 里用到什么,依赖就写什么。不要手动"优化"依赖。
三、依赖太多怎么办?减少依赖,而不是撒谎
当你发现依赖数组越写越长,或者 effect 频繁触发时,正确做法不是删依赖,而是重构代码减少依赖。
方法一:函数式更新
// ❌ 依赖 count,定时器每秒重建
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
// ✅ 不依赖 count,告诉 React "怎么更新"
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
方法二:useReducer
当更新逻辑依赖多个状态时,用 useReducer 把逻辑收拢。dispatch 的引用永远稳定,天然不需要作为依赖。
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // 不依赖任何 state
}, 1000);
return () => clearInterval(id);
}, []);
方法三:处理函数依赖
函数每次渲染都重新创建,放进依赖会导致 effect 每次都跑。三种解法:
| 场景 | 做法 |
|---|---|
| 函数不用 props/state | 移到组件外面 |
| 函数只在 effect 里用 | 移到 effect 里面 |
| 函数在多处使用 | 用 useCallback 包一层 |
四、清理函数看到的是"旧值"
清理函数不等于 componentWillUnmount。它的执行时机是:
React 渲染新 UI
浏览器绘制
运行上一次的清理函数(看到旧 props/state)
运行这一次的 effect(看到新 props/state)
理解这个顺序,就知道为什么清理函数能正确"收拾"上一次的副作用。
五、异步请求防竞态
如果 effect 里有异步请求,ID 变了但旧请求先返回,就会数据错乱。用一个布尔标志位解决:
useEffect(() => {
// 1. 定义一个“取消标志”,只在当前 effect 内部有效
let cancelled = false;
async function fetchData() {
// 2. 发起异步请求
const result = await API.fetch(id);
// 3. 如果没有被取消,才更新数据
if (!cancelled) {
setData(result);
}
}
fetchData();
// 4. effect 清理函数:
// 组件卸载 / id 变化 / effect 重新执行时,会先跑这里
return () => {
cancelled = true;
};
}, [id]); // 依赖 id:id 一变,就重新执行
核心逻辑(超级关键)
-
每次 id 变化,都会生成一个新的 effect
-
旧的 effect 会先执行清理函数:
cancelled = true -
旧请求即使之后返回,看到 cancelled === true,就不会执行 setData
-
只有最新一次的 effect 里的 cancelled 是 false,才会真正更新数据
一句话总结:
只让最后一次请求生效,前面的请求就算回来也扔掉。
**
**
不要想"什么时候该运行 effect",要想"这个 effect 要和哪些状态保持同步"。
旧思维关注过程(挂载了、更新了),新思维关注结果(不管怎么变,UI 和副作用要和当前状态一致)。当你这么想的时候,useEffect 就不再是玄学了。
原文链接:A Complete Guide to useEffect — Dan Abramov