我最近检查了一下项目里的 useEffect,数了一下:37 个。然后我逐个看了一遍,发现其中 29 个根本不需要 useEffect。删掉之后,组件渲染快了,代码短了,bug 也少了。
先看一段"标准"的 React 代码——你可能每天都在写类似的东西:
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*完全多余*。
const [items, setItems] = useState([]);
const [keyword, setKeyword] = useState('');
const filtered = items.filter(item => item.name.includes(keyword));
三行变一行。没有 useEffect,没有额外的 state,没有依赖数组,没有多余的渲染。
这不是个例。大多数 useEffect 都是在用副作用解决本该在渲染时直接算出来的问题。
这是最常见的滥用——把一个可以直接算出来的值,硬拆成 state + useEffect。
// ❌ 用useEffect同步
const [cart, setCart] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(cart.reduce((sum, item) => sum + item.price * item.qty, 0));
}, [cart]);
// ✅ 直接计算
const [cart, setCart] = useState([]);
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
为什么第一种写法有害?
判断标准:这个值能不能从现有的 state/props 直接算出来? 能算就别存。
另一个高频误区——用 useEffect 监听 state 变化来执行"事件逻辑"。
// ❌ useEffect假装事件处理
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
sendAnalytics('form_submit');
showToast('提交成功');
setSubmitted(false);
}
}, [submitted]);
const handleSubmit = () => {
saveForm(data);
setSubmitted(true);
};
// ✅ 直接在事件处理函数里做
const handleSubmit = () => {
saveForm(data);
sendAnalytics('form_submit');
showToast('提交成功');
};
用 useEffect 处理事件有什么问题?
判断标准:这段逻辑是因为"某个事件发生了"才执行的吗? 是的话放事件处理函数里,不要绕道 useEffect。
这个最经典了:
// ❌ 裸写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 行代码。还没处理缓存、重试、去重、竞态条件……
// ✅ 用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空依赖"只执行一次"
useEffect(() => {
initSDK({ appId: 'xxx' });
registerGlobalHandler();
}, []);
看起来人畜无害,但 React 严格模式下会执行两次——SDK 初始化两次,全局 handler 注册两次。
// ✅ 模块级初始化
let initialized = false;
function App() {
if (!initialized) {
initSDK({ appId: 'xxx' });
registerGlobalHandler();
initialized = true;
}
return <div>...</div>;
}
或者用更优雅的方式:
// ✅ 用useRef保证只执行一次
const didInit = useRef(false);
useEffect(() => {
if (didInit.current) return;
didInit.current = true;
initSDK({ appId: 'xxx' });
registerGlobalHandler();
}, []);
判断标准:这段逻辑跟组件的生命周期有关吗? 如果只是"应用启动时执行一次",它不属于任何组件,别放在组件里。
// ❌ 用useEffect让两个state保持同步
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ 派生值直接算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
这跟反模式一本质一样,但出现频率太高了,单独说一遍。
如果你觉得"A 变了 B 也要跟着变",那 B 大概率不是 state,是 A 的计算结果。
说了 5 个"不该用"的,再说说真正该用的场景——和外部系统同步。
// ✅ 正确用法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 标记一下:
删掉那些不该存在的 useEffect,你的组件会变短、变快、bug 更少。这不是优化——是纠正。
你的项目里有多少个 useEffect?检查完之后能删掉几个?评论区聊聊。