关于 dont-use-use-effect
Don't Use useEffect 是面向 React 开发者的技能,指导开发者用其他方式替代不必要的 Effect。内容涵盖派生状态、事件处理、数据获取、订阅和随属性变化重置状态,也说明适合使用 Effect 的场景。
主要功能
- 01从属性和状态派生值
- 02将事件逻辑放入处理函数
- 03采用数据获取替代方案
- 04使用 useSyncExternalStore 订阅
- 05用 key 重置状态
适用场景
- 01检查组件中多余的状态 Effect
- 02选择 React 数据获取模式
- 03替换手动外部存储订阅
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jonmumm/ skills --skill dont-use-use-effect安装后在 Claude Code 中输入/dont-use-use-effect确认已加载。
详细介绍
不要使用 useEffect
useEffect 是 React 中最容易被误用的 Hook 之一。在开发者使用它的绝大多数情况下,都有更简单、性能更好且更正确的替代方案。每一个不必要的 useEffect 都会引入额外的渲染周期,增加出现 bug 的风险(过时闭包、竞态条件、无限循环),并让组件更难理解。
黄金法则: Effect 用于与外部系统同步(DOM API、定时器、websocket、第三方小部件)。如果你正在做的事情是响应用户事件,或者可以根据现有状态/props 计算得出,就不需要 Effect。
1. 派生状态 — 直接计算
最常见的 useEffect 反模式:将可以根据其他状态或 props 计算得出的值存储在状态中。
// 🔴 BAD: Redundant state + unnecessary Effect
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <span>{fullName}</span>;
}
每次 firstName 或 lastName 变化时,这都会导致两次渲染:一次使用过时的 fullName,另一次则发生在 Effect 更新它之后。
// ✅ GOOD: Calculate during render
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// Derived — no state, no Effect, one render
const fullName = firstName + ' ' + lastName;
return <span>{fullName}</span>;
}
对于开销较大的计算,请使用 useMemo 包装,而不是将结果存储在状态中:
// ✅ GOOD: Memoize expensive derivations
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// Only recomputes when todos or filter change, not when newTodo changes
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
return <ul>{visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>;
}
2. 事件驱动逻辑 — 使用事件处理函数
如果代码应该因为用户做了某件事(点击、提交、输入)而运行,就应该放在事件处理函数中,而不是 Effect 中。Effect 是因为组件进行了渲染而运行,而不是因为用户执行了某项操作。
// 🔴 BAD: Event-specific logic triggered by state change in an Effect
function ProductPage({ product, addToCart }) {
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
// ...
}
这会导致 bug:如果页面重新加载时商品已经在购物车中,就会触发通知。
// ✅ GOOD: Side effects belong in the event handler that caused them
function ProductPage({ product, addToCart }) {
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}
经验法则: 如果你能指出应该触发这段代码的确切用户交互,就把代码放在该交互对应的事件处理函数中。
3. Effect 链 — 合并到事件处理函数中
将多个根据其他状态设置状态的 Effect 串联起来,会造成不必要的重复渲染,并使代码变得僵化、脆弱。
// 🔴 BAD: Chain of Effects triggering each other (4 render passes!)
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1);
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) setIsGameOver(true);
}, [round]);
// ...
}
// ✅ GOOD: Derive what you can, compute the rest in the event handler
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// Derived — no state needed
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) throw Error('Game already ended.');
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) alert('Good game!');
}
}
}
// ...
}
4. 数据获取 — 使用数据获取库
在 useEffect 中获取数据很脆弱。你必须手动处理竞态条件、加载状态、缓存、错误处理和清理。直接使用 useEffect 获取数据没有缓存、没有去重,也不支持 SSR。
// 🔴 BAD: Manual fetch in useEffect — race conditions, no caching, no SSR
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
let ignore = false;
setIsLoading(true);
fetchResults(query).then(json => {
if (!ignore) {
setResults(json);
setIsLoading(false);
}
});
return () => { ignore = true; };
}, [query]);
// ...
}
// ✅ GOOD: Use TanStack Query (or your framework's data primitive)
import { useQuery } from '@tanstack/react-query';
function SearchResults({ query }) {
const { data: results = [], isLoading } = useQuery({
queryKey: ['search', query],
queryFn: () => fetchResults(query),
enabled: !!query,
});
// Automatic caching, deduplication, race-condition handling,
// background refetching, and SSR support — for free.
}
其他不错的替代方案:
- Next.js / Remix:
loader函数、Server Components、use()+ Suspense - SWR:
useSWR(key, fetcher)— 与 TanStack Query 类似 - tRPC: 端到端类型安全的数据获取,并内置 React Query 集成
如果你必须使用 useEffect 获取数据(没有框架或库),至少要:
- 添加
ignore清理标记,以防止竞态条件 - 将逻辑提取为可复用的自定义 Hook(例如
useData(url)) - 明确处理加载、错误和空状态
5. 外部存储订阅 — 使用 useSyncExternalStore
使用 useEffect + setState 订阅外部数据源(浏览器 API、第三方存储、可观察对象),不仅需要编写大量样板代码,还可能在并发模式下导致 tearing。
// 🔴 BAD: Manual subscription with useEffect
function OnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return <span>{isOnline ? '✅ Online' : '❌ Offline'}</span>;
}
// ✅ GOOD: useSyncExternalStore — concurrent-safe, less code
import { useSyncExternalStore } from 'react';
function subscribe(callback: () => void) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function OnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return <span>{isOnline ? '✅ Online' : '❌ Offline'}</span>;
}
对于状态管理库,请使用其内置的 React Hook,而不是手动订阅。请参阅 react-render-performance 技能,了解使用 XState、Zustand、Redux 和 Nanostores 的基于选择器的模式。
6. 属性变化时重置状态 — 使用 key
不要使用 useEffect 在 prop 变化时重置状态。React 内置了一种机制:key prop。
// 🔴 BAD: Resetting state in an Effect
function EditProfile({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}
// ✅ GOOD: Use key to reset component state
function ProfilePage({ userId }) {
// When userId changes, React unmounts the old EditProfile and mounts a new one
return <EditProfile userId={userId} key={userId} />;
}
function EditProfile({ userId }) {
const [comment, setComment] = useState('');
return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
}
useEffect 适用的情况
当需要将组件与React 之外的事物同步时,使用 Effect 是合适的:
| 使用场景 | 为什么合适 |
|---|---|
| 集成非 React 组件(地图、图表、视频播放器) | 将 React 状态同步到命令式 DOM API |
| 建立 WebSocket 或 EventSource 连接 | 外部系统的生命周期与组件绑定 |
测量 DOM 布局(getBoundingClientRect、ResizeObserver) | 读取渲染后的 DOM 信息 |
| 以命令式方式管理焦点或滚动位置 | 命令式 DOM 交互 |
| 连接硬件 API(摄像头、地理位置) | 具有订阅和清理操作的外部系统 |
document.title 或其他全局副作用 | 将 React 状态同步到浏览器 API |
即使在这些情况下,也要考虑是否已有库可以处理这些工作(例如,react-intersection-observer、framer-motion)。
决策流程图
编写 useEffect 之前,请先问自己以下问题:
- 能否根据现有 props/state 计算出这个值? → 在渲染期间推导(或使用
useMemo)。 - 这是对用户操作的响应吗? → 将逻辑放在事件处理函数中。
- 我在获取数据吗? → 使用 TanStack Query、SWR 或框架的数据原语。
- 我在订阅外部存储吗? → 使用
useSyncExternalStore或库提供的 Hook。 - 我在 prop 发生变化时重置状态吗? → 使用
keyprop。 - 我在与 React 无法控制的外部系统同步吗? → ✅ 这是有效的
useEffect用法。
总结检查清单
- 不要使用
useEffect来设置从其他状态/props 推导出的状态——应改为在渲染期间计算 - 不要使用
useEffect来响应用户事件执行逻辑——应改为使用事件处理函数 - 不要让多个
useEffect通过状态更新相互触发形成调用链——应改为合并到事件处理函数中,或使用推导值 - 不要使用原始
useEffect获取数据——应改为使用 TanStack Query、SWR 或框架的数据原语 - 不要使用
useEffect+setState订阅外部存储——应改为使用useSyncExternalStore或库提供的 Hook - 不要在 props 发生变化时使用
useEffect重置状态——应改为使用keyprop - 所有保留的
useEffect都应与真正的外部系统(DOM API、WebSocket、第三方组件)同步