我最近检查了一下项目里的useEffect,数了一下:37个。然后我逐个看了一遍,发现其中29个根本不需要useEffect。删掉之后,组件渲染快了,代码短了,bug也少了。
你大概也是这样写的
先看一段"标准"的React代码------你可能每天都在写类似的东西:
tsx
const [items, setItems] = useState([]);
const [filtered, setFiltered] = useState([]);
const [keyword, setKeyword] = useState('');
useEffect(() => {
setFiltered(items.filter(item => item.name.includes(keyword)));
}, [items, keyword]);
看起来很合理对吧?keyword变了,重新过滤一遍。
但这个useEffect完全多余。
tsx
const [items, setItems] = useState([]);
const [keyword, setKeyword] = useState('');
const filtered = items.filter(item => item.name.includes(keyword));
三行变一行。没有useEffect,没有额外的state,没有依赖数组,没有多余的渲染。
这不是个例。大多数useEffect都是在用副作用解决本该在渲染时直接算出来的问题。
反模式一:用useEffect同步派生状态
这是最常见的滥用------把一个可以直接算出来的值,硬拆成state + useEffect。
tsx
// ❌ 用useEffect同步
const [cart, setCart] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(cart.reduce((sum, item) => sum + item.price * item.qty, 0));
}, [cart]);
tsx
// ✅ 直接计算
const [cart, setCart] = useState([]);
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
为什么第一种写法有害?
- 多一次渲染:cart变了 → 渲染一次 → useEffect触发setTotal → 再渲染一次。两次渲染干了一次的活
- 多一个state:total是cart的派生值,但你给了它独立的生命周期,就有可能出现cart和total不同步的瞬间
- 多一个依赖数组:多一个需要维护的东西,多一个可能写错的地方
判断标准:这个值能不能从现有的state/props直接算出来? 能算就别存。
反模式二:用useEffect响应事件
另一个高频误区------用useEffect监听state变化来执行"事件逻辑"。
tsx
// ❌ useEffect假装事件处理
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
sendAnalytics('form_submit');
showToast('提交成功');
setSubmitted(false);
}
}, [submitted]);
const handleSubmit = () => {
saveForm(data);
setSubmitted(true);
};
tsx
// ✅ 直接在事件处理函数里做
const handleSubmit = () => {
saveForm(data);
sendAnalytics('form_submit');
showToast('提交成功');
};
用useEffect处理事件有什么问题?
- 因果关系断裂:读代码时,你看到setSubmitted(true),根本不知道它会触发什么。逻辑分散在两个地方
- 执行时机不可控:useEffect在渲染后执行,不是在事件发生时执行。中间隔了一次渲染
- 临时state泄露:submitted这个state只是为了"传信号",没有UI含义
判断标准:这段逻辑是因为"某个事件发生了"才执行的吗? 是的话放事件处理函数里,不要绕道useEffect。
反模式三:用useEffect获取数据(没有用对工具)
这个最经典了:
tsx
// ❌ 裸写useEffect获取数据
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUser(userId)
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [userId]);
一个简单的数据获取,写了20行代码。还没处理缓存、重试、去重、竞态条件......
tsx
// ✅ 用React Query
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
三行搞定,还自带缓存、重试、去重、窗口聚焦刷新、竞态处理。
不是说"永远不要在useEffect里fetch"。 是说如果你的项目有超过3个数据获取的场景,裸写useEffect就是在重复造一个质量更差的React Query。
| 特性 | 裸写useEffect | React Query / SWR |
|---|---|---|
| 缓存 | 自己写 | 内置 |
| 去重 | 自己写 | 内置 |
| 竞态处理 | 容易忘 | 内置 |
| 重试 | 自己写 | 内置 |
| loading/error | 3个useState | 返回值直接拿 |
| 代码量 | 20+行 | 3-5行 |
反模式四:用useEffect初始化只执行一次的逻辑
tsx
// ❌ useEffect空依赖"只执行一次"
useEffect(() => {
initSDK({ appId: 'xxx' });
registerGlobalHandler();
}, []);
看起来人畜无害,但React严格模式下会执行两次------SDK初始化两次,全局handler注册两次。
tsx
// ✅ 模块级初始化
let initialized = false;
function App() {
if (!initialized) {
initSDK({ appId: 'xxx' });
registerGlobalHandler();
initialized = true;
}
return <div>...</div>;
}
或者用更优雅的方式:
tsx
// ✅ 用useRef保证只执行一次
const didInit = useRef(false);
useEffect(() => {
if (didInit.current) return;
didInit.current = true;
initSDK({ appId: 'xxx' });
registerGlobalHandler();
}, []);
判断标准:这段逻辑跟组件的生命周期有关吗? 如果只是"应用启动时执行一次",它不属于任何组件,别放在组件里。
反模式五:用useEffect"同步"两个state
tsx
// ❌ 用useEffect让两个state保持同步
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
tsx
// ✅ 派生值直接算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
这跟反模式一本质一样,但出现频率太高了,单独说一遍。
如果你觉得"A变了B也要跟着变",那B大概率不是state,是A的计算结果。
那什么时候该用useEffect?
说了5个"不该用"的,再说说真正该用的场景------和外部系统同步。
tsx
// ✅ 正确用法1:操作DOM
useEffect(() => {
const el = ref.current;
const observer = new ResizeObserver(entries => {
setWidth(entries[0].contentRect.width);
});
observer.observe(el);
return () => observer.disconnect();
}, []);
// ✅ 正确用法2:WebSocket连接
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (e) => setMessages(prev => [...prev, JSON.parse(e.data)]);
return () => ws.close();
}, [url]);
// ✅ 正确用法3:第三方库集成
useEffect(() => {
const chart = new Chart(canvasRef.current, config);
return () => chart.destroy();
}, [config]);
useEffect的正确用途只有一个:让React组件和React之外的东西保持同步。 DOM API、WebSocket、第三方库、浏览器API------这些才需要useEffect。
如果你的useEffect里调用的全是React自己的API(setState、其他hooks),那大概率用错了。
终极速查表
| 你想做什么 | 用useEffect? | 应该怎么做 |
|---|---|---|
| 从state/props计算一个值 | ❌ | 渲染时直接计算,或useMemo |
| 用户点了按钮要执行逻辑 | ❌ | 写在事件处理函数里 |
| 请求后端API | ❌ | React Query / SWR / 框架的loader |
| 表单提交后发埋点 | ❌ | 写在onSubmit里 |
| props变了要重置组件state | ❌ | 给组件加key |
| 两个state要保持同步 | ❌ | 合并成一个state,或算派生值 |
| 应用启动时初始化SDK | ⚠️ | 模块级代码或useRef守卫 |
| 监听window resize | ✅ | useEffect + cleanup |
| 连接WebSocket | ✅ | useEffect + cleanup |
| 集成D3/Chart等第三方库 | ✅ | useEffect + cleanup |
| 同步document.title | ✅ | useEffect |
一句话总结:useEffect不是"state变了执行代码"的工具,是"和外部世界同步"的工具。
自查清单
回去看看你的项目,把每个useEffect标记一下:
- 里面有没有setState?→ 大概率可以删
- 是不是在响应某个事件?→ 移到事件处理函数
- 是不是在fetch数据?→ 换React Query
- 依赖数组是不是总出bug?→ 可能根本不该用useEffect
- 删掉这个useEffect,组件还能正常工作吗?→ 能的话就别犹豫
删掉那些不该存在的useEffect,你的组件会变短、变快、bug更少。这不是优化------是纠正。
你的项目里有多少个useEffect?检查完之后能删掉几个?评论区聊聊。