Appearance
useEffect常见错误 - React副作用管理完全指南
1. 依赖数组错误
1.1 缺少依赖项
jsx
// ❌ 错误: 缺少count依赖
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log('Current count:', count); // count永远是0
}, 1000);
return () => clearInterval(timer);
}, []); // 缺少count依赖
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// ✅ 解决方案1: 添加依赖
function CounterSolution1() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log('Current count:', count);
}, 1000);
return () => clearInterval(timer);
}, [count]); // 添加count依赖
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// ✅ 解决方案2: 使用函数式更新
function CounterSolution2() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => {
console.log('Current count:', c);
return c;
});
}, 1000);
return () => clearInterval(timer);
}, []); // 不需要count依赖
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// ✅ 解决方案3: 使用useRef
function CounterSolution3() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
console.log('Current count:', countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}1.2 错误的依赖项
jsx
// ❌ 错误: 对象/数组作为依赖
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const options = { // 每次渲染都是新对象
include: 'details'
};
useEffect(() => {
// 因为options每次都是新对象,effect会无限执行
fetchUser(userId, options).then(setUser);
}, [userId, options]); // options引用每次都变
return <div>{user?.name}</div>;
}
// ✅ 解决方案1: 将对象移到effect内部
function UserProfileSolution1({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const options = {
include: 'details'
};
fetchUser(userId, options).then(setUser);
}, [userId]); // 只依赖userId
return <div>{user?.name}</div>;
}
// ✅ 解决方案2: 使用useMemo缓存
function UserProfileSolution2({ userId }) {
const [user, setUser] = useState(null);
const options = useMemo(() => ({
include: 'details'
}), []); // 空依赖,options引用不变
useEffect(() => {
fetchUser(userId, options).then(setUser);
}, [userId, options]);
return <div>{user?.name}</div>;
}
// ✅ 解决方案3: 只依赖必要的值
function UserProfileSolution3({ userId }) {
const [user, setUser] = useState(null);
const includeDetails = true;
useEffect(() => {
const options = {
include: includeDetails ? 'details' : 'basic'
};
fetchUser(userId, options).then(setUser);
}, [userId, includeDetails]); // 依赖原始值
return <div>{user?.name}</div>;
}1.3 函数作为依赖
jsx
// ❌ 错误: 函数在每次渲染时都是新的
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const searchAPI = (q) => { // 每次渲染都是新函数
return fetch(`/api/search?q=${q}`).then(r => r.json());
};
useEffect(() => {
// 因为searchAPI每次都是新函数,effect会无限执行
if (query) {
searchAPI(query).then(setResults);
}
}, [query, searchAPI]); // searchAPI引用每次都变
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{results.map(r => <div key={r.id}>{r.title}</div>)}
</div>
);
}
// ✅ 解决方案1: 使用useCallback
function SearchComponentSolution1() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const searchAPI = useCallback((q) => {
return fetch(`/api/search?q=${q}`).then(r => r.json());
}, []); // 空依赖,函数引用不变
useEffect(() => {
if (query) {
searchAPI(query).then(setResults);
}
}, [query, searchAPI]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{results.map(r => <div key={r.id}>{r.title}</div>)}
</div>
);
}
// ✅ 解决方案2: 将函数移到effect内部
function SearchComponentSolution2() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (query) {
const searchAPI = (q) => {
return fetch(`/api/search?q=${q}`).then(r => r.json());
};
searchAPI(query).then(setResults);
}
}, [query]); // 只依赖query
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{results.map(r => <div key={r.id}>{r.title}</div>)}
</div>
);
}2. 清理函数错误
2.1 忘记清理副作用
jsx
// ❌ 错误: 忘记清理定时器
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// 忘记返回清理函数!
// 组件卸载后定时器仍在运行,导致内存泄漏
}, []);
return <div>Seconds: {seconds}</div>;
}
// ✅ 正确: 清理定时器
function TimerCorrect() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => {
clearInterval(timer); // 组件卸载时清理
};
}, []);
return <div>Seconds: {seconds}</div>;
}2.2 忘记取消订阅
jsx
// ❌ 错误: 忘记取消事件监听
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// 忘记移除监听器!
}, []);
return <div>{size.width} x {size.height}</div>;
}
// ✅ 正确: 清理事件监听
function WindowSizeCorrect() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <div>{size.width} x {size.height}</div>;
}2.3 异步操作的清理
jsx
// ❌ 错误: 异步操作完成后设置已卸载组件的状态
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(data => {
// 如果组件已卸载,这里会报警告
setUser(data);
});
}, [userId]);
return <div>{user?.name}</div>;
}
// ✅ 正确: 使用取消标志
function UserProfileCorrect({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) {
setUser(data);
}
});
return () => {
cancelled = true;
};
}, [userId]);
return <div>{user?.name}</div>;
}
// ✅ 更好: 使用AbortController
function UserProfileBetter({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const abortController = new AbortController();
fetch(`/api/users/${userId}`, {
signal: abortController.signal
})
.then(res => res.json())
.then(data => setUser(data))
.catch(error => {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
});
return () => {
abortController.abort();
};
}, [userId]);
return <div>{user?.name}</div>;
}3. 无限循环
3.1 缺少依赖数组
jsx
// ❌ 错误: 没有依赖数组
function InfiniteLoop() {
const [count, setCount] = useState(0);
useEffect(() => {
// 每次渲染都执行,导致无限循环
setCount(count + 1);
}); // 缺少依赖数组!
return <div>{count}</div>;
}
// ✅ 正确: 添加空依赖数组
function NoInfiniteLoop() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []); // 只在mount时执行一次
return <div>{count}</div>;
}3.2 依赖项导致循环
jsx
// ❌ 错误: 在effect中更新依赖项
function DataFetcher() {
const [data, setData] = useState([]);
const [config, setConfig] = useState({ page: 1 });
useEffect(() => {
fetchData(config).then(newData => {
setData(newData);
// 错误!更新了config,触发新的effect执行
setConfig({ ...config, page: config.page + 1 });
});
}, [config]); // config变化触发effect,effect又更新config
return <div>{data.length} items</div>;
}
// ✅ 解决方案1: 移除不必要的依赖
function DataFetcherSolution1() {
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
fetchData({ page }).then(newData => {
setData(newData);
});
}, [page]); // 只依赖page
const loadMore = () => {
setPage(p => p + 1); // 在外部更新page
};
return (
<div>
<div>{data.length} items</div>
<button onClick={loadMore}>Load More</button>
</div>
);
}
// ✅ 解决方案2: 使用useReducer
function dataReducer(state, action) {
switch (action.type) {
case 'FETCH_SUCCESS':
return {
...state,
data: action.data,
page: state.page + 1
};
default:
return state;
}
}
function DataFetcherSolution2() {
const [state, dispatch] = useReducer(dataReducer, {
data: [],
page: 1
});
useEffect(() => {
fetchData({ page: state.page }).then(data => {
dispatch({ type: 'FETCH_SUCCESS', data });
});
}, [state.page]);
return <div>{state.data.length} items</div>;
}4. 竞态条件
4.1 快速切换导致的竞态
jsx
// ❌ 错误: 未处理竞态条件
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
// 如果query快速变化,可能显示错误的结果
searchAPI(query).then(data => {
// 后返回的请求可能不是最新query的结果
setResults(data);
});
}, [query]);
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
// ✅ 解决方案1: 使用取消标志
function SearchResultsSolution1({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false;
searchAPI(query).then(data => {
if (!cancelled) {
setResults(data);
}
});
return () => {
cancelled = true;
};
}, [query]);
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
// ✅ 解决方案2: 使用AbortController
function SearchResultsSolution2({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const abortController = new AbortController();
fetch(`/api/search?q=${query}`, {
signal: abortController.signal
})
.then(res => res.json())
.then(data => setResults(data))
.catch(error => {
if (error.name !== 'AbortError') {
console.error('Search error:', error);
}
});
return () => {
abortController.abort();
};
}, [query]);
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
// ✅ 解决方案3: 使用请求ID
function SearchResultsSolution3({ query }) {
const [results, setResults] = useState([]);
const requestIdRef = useRef(0);
useEffect(() => {
const requestId = ++requestIdRef.current;
searchAPI(query).then(data => {
// 只处理最新请求的结果
if (requestId === requestIdRef.current) {
setResults(data);
}
});
}, [query]);
return (
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}5. 过度使用useEffect
5.1 不必要的effect
jsx
// ❌ 错误: 用effect计算派生状态
function TodoList({ todos }) {
const [completedCount, setCompletedCount] = useState(0);
useEffect(() => {
// 不必要的effect!
const count = todos.filter(t => t.completed).length;
setCompletedCount(count);
}, [todos]);
return (
<div>
<p>{completedCount} completed</p>
{todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
);
}
// ✅ 正确: 直接计算
function TodoListCorrect({ todos }) {
// 直接计算,不需要effect
const completedCount = todos.filter(t => t.completed).length;
return (
<div>
<p>{completedCount} completed</p>
{todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
);
}
// ✅ 如果计算昂贵,使用useMemo
function TodoListOptimized({ todos }) {
const completedCount = useMemo(() => {
return todos.filter(t => t.completed).length;
}, [todos]);
return (
<div>
<p>{completedCount} completed</p>
{todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
);
}5.2 用effect同步props到state
jsx
// ❌ 错误: props变化时用effect更新state
function EmailInput({ defaultEmail }) {
const [email, setEmail] = useState(defaultEmail);
useEffect(() => {
// 不必要的effect!
setEmail(defaultEmail);
}, [defaultEmail]);
return (
<input
value={email}
onChange={e => setEmail(e.target.value)}
/>
);
}
// ✅ 解决方案1: 完全受控
function EmailInputControlled({ email, onChange }) {
return (
<input
value={email}
onChange={e => onChange(e.target.value)}
/>
);
}
// ✅ 解决方案2: 使用key重置
function Parent() {
const [email, setEmail] = useState('john@example.com');
return (
<EmailInput
key={email} // key变化会重新创建组件
defaultEmail={email}
/>
);
}
// ✅ 解决方案3: 派生状态
function EmailInputDerived({ defaultEmail }) {
const [isEditing, setIsEditing] = useState(false);
const [draftEmail, setDraftEmail] = useState('');
// 派生显示的email
const displayEmail = isEditing ? draftEmail : defaultEmail;
const handleFocus = () => {
setIsEditing(true);
setDraftEmail(defaultEmail);
};
const handleBlur = () => {
setIsEditing(false);
};
return (
<input
value={displayEmail}
onChange={e => setDraftEmail(e.target.value)}
onFocus={handleFocus}
onBlur={handleBlur}
/>
);
}6. 执行时机错误
6.1 useEffect vs useLayoutEffect
jsx
// ❌ 错误: 需要同步测量DOM时使用useEffect
function MeasureHeight() {
const [height, setHeight] = useState(0);
const ref = useRef(null);
useEffect(() => {
// useEffect是异步的,可能看到闪烁
setHeight(ref.current.offsetHeight);
}, []);
return (
<div ref={ref} style={{ height: height > 100 ? '100px' : 'auto' }}>
Content
</div>
);
}
// ✅ 正确: 使用useLayoutEffect同步测量
function MeasureHeightCorrect() {
const [height, setHeight] = useState(0);
const ref = useRef(null);
useLayoutEffect(() => {
// useLayoutEffect在DOM更新后同步执行
setHeight(ref.current.offsetHeight);
}, []);
return (
<div ref={ref} style={{ height: height > 100 ? '100px' : 'auto' }}>
Content
</div>
);
}6.2 在effect中调用setState
jsx
// ❌ 错误: 条件可能永不满足
function DataLoader() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then(result => {
setData(result);
// 错误!如果data不是真值,loading永远不会变false
if (data) {
setLoading(false);
}
});
}, []);
return loading ? <div>Loading...</div> : <div>{data}</div>;
}
// ✅ 正确: 使用局部变量
function DataLoaderCorrect() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then(result => {
setData(result);
// 使用result判断,不依赖state
if (result) {
setLoading(false);
}
});
}, []);
return loading ? <div>Loading...</div> : <div>{data}</div>;
}7. 依赖项对比错误
7.1 对象比较
jsx
// ❌ 错误: 每次渲染都是新对象
function UserList() {
const [users, setUsers] = useState([]);
const filter = { // 每次都是新对象
status: 'active'
};
useEffect(() => {
// filter每次都不同,导致无限请求
fetchUsers(filter).then(setUsers);
}, [filter]);
return <div>{users.length} users</div>;
}
// ✅ 解决方案1: 使用原始值
function UserListSolution1() {
const [users, setUsers] = useState([]);
const status = 'active'; // 原始值
useEffect(() => {
fetchUsers({ status }).then(setUsers);
}, [status]); // 原始值比较正确
return <div>{users.length} users</div>;
}
// ✅ 解决方案2: useMemo缓存
function UserListSolution2() {
const [users, setUsers] = useState([]);
const filter = useMemo(() => ({
status: 'active'
}), []); // 对象引用不变
useEffect(() => {
fetchUsers(filter).then(setUsers);
}, [filter]);
return <div>{users.length} users</div>;
}
// ✅ 解决方案3: 自定义比较
function useEffectDeep(effect, deps) {
const ref = useRef();
const signalRef = useRef(0);
if (!deepEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}
useEffect(effect, [signalRef.current]);
}
function UserListSolution3() {
const [users, setUsers] = useState([]);
const filter = {
status: 'active'
};
useEffectDeep(() => {
fetchUsers(filter).then(setUsers);
}, [filter]); // 深度比较
return <div>{users.length} users</div>;
}8. 内存泄漏
8.1 未清理的订阅
jsx
// ❌ 错误: WebSocket未关闭
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(`ws://chat.server/${roomId}`);
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
// 忘记关闭WebSocket!
}, [roomId]);
return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}
// ✅ 正确: 清理WebSocket
function ChatRoomCorrect({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket(`ws://chat.server/${roomId}`);
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
return () => {
ws.close(); // 清理连接
};
}, [roomId]);
return (
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
);
}8.2 定时器泄漏
jsx
// ❌ 错误: 多个定时器累积
function Poller({ interval }) {
const [data, setData] = useState(null);
useEffect(() => {
const timer = setInterval(() => {
fetchData().then(setData);
}, interval);
// 忘记清理!
// interval变化时创建新定时器,旧的仍在运行
}, [interval]);
return <div>{data}</div>;
}
// ✅ 正确: 清理旧定时器
function PollerCorrect({ interval }) {
const [data, setData] = useState(null);
useEffect(() => {
const timer = setInterval(() => {
fetchData().then(setData);
}, interval);
return () => {
clearInterval(timer); // 清理旧定时器
};
}, [interval]);
return <div>{data}</div>;
}9. 调试技巧
9.1 effect执行追踪
jsx
function useEffectDebug(effect, deps, name) {
const prevDeps = useRef(deps);
useEffect(() => {
console.group(`[useEffect] ${name}`);
if (prevDeps.current) {
const changedDeps = deps.map((dep, i) => {
if (dep !== prevDeps.current[i]) {
return {
index: i,
from: prevDeps.current[i],
to: dep
};
}
return null;
}).filter(Boolean);
if (changedDeps.length > 0) {
console.log('Changed dependencies:', changedDeps);
}
} else {
console.log('Initial mount');
}
prevDeps.current = deps;
const cleanup = effect();
console.groupEnd();
return cleanup;
}, deps);
}
// 使用
function Component({ userId }) {
const [user, setUser] = useState(null);
useEffectDebug(() => {
fetchUser(userId).then(setUser);
}, [userId], 'Fetch User');
return <div>{user?.name}</div>;
}9.2 effect cleanup追踪
jsx
function useEffectWithCleanupDebug(effect, deps, name) {
useEffect(() => {
console.log(`[Effect] ${name} - Running`);
const cleanup = effect();
return () => {
console.log(`[Effect] ${name} - Cleanup`);
if (cleanup) cleanup();
};
}, deps);
}10. 最佳实践
10.1 检查清单
typescript
const useEffectChecklist = [
'✅ 是否需要cleanup函数?',
'✅ 依赖数组是否完整?',
'✅ 是否有竞态条件?',
'✅ 是否会导致无限循环?',
'✅ 是否真的需要useEffect?',
'✅ 是否应该用useLayoutEffect?',
'✅ 是否有内存泄漏风险?',
'✅ 异步操作是否正确取消?',
'✅ 是否考虑了组件卸载场景?',
'✅ 依赖项是否使用了正确的比较方式?'
];10.2 常见模式
jsx
// 模式1: 数据获取
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetch(url)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// 模式2: 订阅
function useSubscription(subscribe) {
const [value, setValue] = useState(null);
useEffect(() => {
const unsubscribe = subscribe(setValue);
return unsubscribe;
}, [subscribe]);
return value;
}
// 模式3: 事件监听
function useEventListener(event, handler, element = window) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (e) => savedHandler.current(e);
element.addEventListener(event, eventListener);
return () => {
element.removeEventListener(event, eventListener);
};
}, [event, element]);
}11. 总结
useEffect常见错误的核心要点:
- 依赖数组: 确保所有依赖都包含在内
- 清理函数: 始终清理副作用
- 竞态条件: 处理异步操作的取消
- 无限循环: 避免在effect中更新依赖项
- 过度使用: 考虑是否真的需要effect
- 执行时机: 正确选择useEffect或useLayoutEffect
- 对象比较: 注意对象/数组的引用比较
- 内存泄漏: 清理所有订阅和定时器
- 调试技巧: 使用自定义Hook追踪effect
- 最佳实践: 遵循React官方建议
掌握这些知识可以避免useEffect的常见陷阱,写出更健壮的React应用。