
为什么 React Hooks 容易出错
React Hooks 引入了一种新的心智模型,组件状态和副作用与函数调用绑定。大多数 bug 源于未能完全理解闭包、依赖数组和 React 渲染周期之间的交互。
本指南涵盖了最常见的错误及其修复方法。

错误 1:useEffect 中的闭包过期
这是最常见且最难调试的 React Hook bug。
// ❌ Bug:count 已过期——在 interval 内部始终为 0
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
console.log('Count:', count); // 始终为 0
setCount(count + 1); // 始终设置为 1 (0 + 1)
}, 1000);
return () => clearInterval(interval);
}, []); // [] 表示“只运行一次”——闭包永远捕获 count=0
return <div>{count}</div>;
}
修复 1:使用函数式状态更新器
// ✅ 函数式更新不需要捕获 count
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // 始终获取当前值
}, 1000);
return () => clearInterval(interval);
}, []);
修复 2:添加到依赖数组(导致重新注册)
// ✅ 当 count 变化时 effect 重新运行
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // 现在使用当前的 count
}, 1000);
return () => clearInterval(interval);
}, [count]); // 但每次 count 变化都会重新创建 interval
修复 3:使用 useRef 存储可变值
// ✅ ref 始终是最新的,不会触发重渲染
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const interval = setInterval(() => {
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
错误 2:useEffect 依赖不正确
ESLint 的 react-hooks/exhaustive-deps 规则标记缺失依赖是有原因的。
// ❌ 缺少依赖:userId
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Bug:当 userId 变化时不会重新获取
return <div>{user?.name}</div>;
}
// ✅ 将 userId 包含在依赖中
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // 当 userId 变化时重新获取
对象和函数依赖
// ❌ Bug:options 对象在每次渲染时重新创建
function DataFetcher({ userId }) {
const options = { headers: { 'X-User': userId } }; // 每次渲染新对象
useEffect(() => {
fetch('/api/data', options).then(/* ... */);
}, [options]); // 无限循环!options 始终在变化
}
// ✅ 修复 1:将对象移到 effect 内部
useEffect(() => {
const options = { headers: { 'X-User': userId } };
fetch('/api/data', options).then(/* ... */);
}, [userId]); // 只依赖原始值
// ✅ 修复 2:使用 useMemo 稳定对象
const options = useMemo(
() => ({ headers: { 'X-User': userId } }),
[userId]
);

错误 3:无限重渲染循环
// ❌ 无限循环:effect 设置状态,导致重渲染,再次运行 effect
function BadComponent() {
const [data, setData] = useState([]);
useEffect(() => {
setData([...data, 'item']); // 触发重渲染 → effect 再次运行
}); // 没有依赖数组 = 每次渲染后运行
return <div>{data.length}</div>;
}
// ❌ 无限循环:依赖中的对象每次渲染重新创建
function AlsoBad() {
const [result, setResult] = useState(null);
const config = { timeout: 5000 }; // 每次渲染新对象
useEffect(() => {
fetch('/api', config).then(r => r.json()).then(setResult);
}, [config]); // config 每次渲染都变化 → 无限循环
}
// ✅ 修复:使用 useMemo、useCallback 或将值移到 effect 内部
const config = useMemo(() => ({ timeout: 5000 }), []); // 稳定引用
错误 4:直接修改状态
React 状态更新只有在替换引用时才会触发重渲染,原地修改不会。
// ❌ 修改:React 检测不到变化
const [items, setItems] = useState(['a', 'b', 'c']);
function addItem() {
items.push('d'); // 修改数组
setItems(items); // 相同引用!React 跳过更新
}
// ✅ 创建新数组
function addItem() {
setItems([...items, 'd']); // 新引用 → 重渲染
}
// ❌ 对象修改
const [user, setUser] = useState({ name: 'Alice', age: 30 });
user.name = 'Bob'; // 修改
setUser(user); // 相同引用,不重渲染
// ✅ 使用展开创建新对象
setUser({ ...user, name: 'Bob' });
嵌套对象更新
const [profile, setProfile] = useState({
user: { name: 'Alice', address: { city: 'NYC', zip: '10001' } }
});
// ❌ Bug:嵌套修改
profile.user.address.city = 'LA';
setProfile(profile);
// ✅ 深层展开
setProfile({
...profile,
user: {
...profile.user,
address: {
...profile.user.address,
city: 'LA',
},
},
});
// ✅ 更好的做法:使用 Immer
import { produce } from 'immer';
setProfile(produce(draft => {
draft.user.address.city = 'LA'; // Immer 使修改安全
}));

错误 5:过度使用 useCallback / useMemo
// ❌ 无意义的记忆化——比什么都不做更消耗资源
function SimpleComponent({ value }) {
// 包装一个简单函数浪费内存和 CPU
const add = useCallback((a, b) => a + b, []); // 直接写:const add = (a, b) => a + b
const doubled = useMemo(() => value * 2, [value]); // 直接写:const doubled = value * 2
}
// ✅ useCallback 真正有用时:传递给记忆化子组件的回调
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 稳定引用 → 防止 MemoizedChild 重渲染
return <MemoizedChild onClick={handleClick} />;
// ✅ useMemo 真正有用时:昂贵计算
const filteredItems = useMemo(
() => items.filter(item => item.category === category && item.price < maxPrice),
[items, category, maxPrice]
); // 仅在输入变化时重新计算,而非每次渲染
错误 6:条件调用 Hooks
// ❌ 违反 Hooks 规则
function BadComponent({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // 条件调用 hook
}
// ...
}
// ✅ 始终在顶层调用 hooks,在内部使用条件
function GoodComponent({ isLoggedIn }) {
const [user, setUser] = useState(null); // 始终调用
useEffect(() => {
if (!isLoggedIn) return; // 条件在 effect 内部
fetchUser().then(setUser);
}, [isLoggedIn]);
}
错误 7:未清理 Effect
// ❌ 内存泄漏 / 竞态条件:在已卸载组件上设置状态
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data); // 可能在组件卸载后运行
});
}, [userId]);
}
// ✅ 使用 AbortController 在清理时取消请求
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') throw err; // 忽略中止错误
});
return () => controller.abort(); // 清理:取消正在进行的请求
}, [userId]);
错误 8:useState 处理复杂对象——考虑 useReducer
// ❌ 多个相关状态更新不同步
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
async function loadData() {
setLoading(true);
try {
const result = await fetch('/api');
setData(result); // 这三个更新导致 3 次渲染
setLoading(false);
setError(null);
} catch (err) {
setError(err);
setLoading(false);
}
}
// ✅ useReducer 将相关状态更新分组
const [state, dispatch] = useReducer(reducer, {
status: 'idle', // 'idle' | 'loading' | 'success' | 'error'
data: null,
error: null,
});
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START': return { ...state, status: 'loading', error: null };
case 'FETCH_SUCCESS': return { status: 'success', data: action.payload, error: null };
case 'FETCH_ERROR': return { ...state, status: 'error', error: action.error };
default: return state;
}
}
async function loadData() {
dispatch({ type: 'FETCH_START' });
try {
const data = await fetch('/api').then(r => r.json());
dispatch({ type: 'FETCH_SUCCESS', payload: data }); // 一次原子更新
} catch (err) {
dispatch({ type: 'FETCH_ERROR', error: err });
}
}
快速参考
| Bug | 根本原因 | 修复方法 |
|---|---|---|
| Effect 中的值过期 | 闭包捕获了旧状态 | 函数式更新器或正确的依赖 |
| Effect 运行过于频繁 | 依赖中存在不稳定的对象/函数 | useMemo / useCallback / 移到 effect 内部 |
| 无限重渲染 | 依赖中的对象/数组每次渲染重新创建 | useMemo 或移到外部作用域 |
| 状态变化未触发重渲染 | 直接修改 | 始终替换,绝不修改 |
| 请求竞态条件 | 没有清理 | AbortController + 清理函数 |
| Hooks 顺序错误 | 条件调用 hook | 始终无条件调用 hooks |
→ 使用 JSON Viewer 检查数据结构来调试组件状态。