引言
React Hooks 自 2019 年推出以来,彻底改变了我们编写 React 组件的方式。它们让函数组件拥有了状态管理和生命周期能力,代码更加简洁优雅。然而,Hooks 的使用并非没有陷阱——错误的依赖数组、滥用 useState、忽略清理函数等问题常常导致难以追踪的 bug。
本文将从实际项目经验出发,分享 React Hooks 的最佳实践,并剖析那些容易踩坑的地方,帮助你写出更健壮的代码。
一、useEffect 依赖数组的正確使用
常见错误
// ❌ 错误:遗漏依赖项
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // userId 变化时不会重新请求
return <div>{user?.name}</div>;
}
正确做法
// ✅ 正确:完整声明依赖
function UserProfile({ 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>;
}
要点:
- 依赖数组必须包含所有在 effect 中使用的响应式值
- 使用 ESLint 插件
eslint-plugin-react-hooks自动检查 - 对于异步操作,务必添加清理函数防止状态更新在组件卸载后执行
二、useState 的状态设计原则
避免派生状态
// ❌ 错误:存储可计算的值
function Cart({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);
return <div>总计:{total}</div>;
}
// ✅ 正确:直接计算
function Cart({ items }) {
const total = items.reduce((sum, item) => sum + item.price, 0);
return <div>总计:{total}</div>;
}
状态合并策略
// ❌ 多个相关状态
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
// ✅ 合并为单一状态对象
const [state, setState] = useState({
loading: false,
error: null,
data: null
});
// 或使用 useReducer 处理复杂状态
三、自定义 Hooks 的复用艺术
提取通用逻辑
// useLocalStorage.js
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// 使用示例
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
当前主题:{theme}
</button>
);
}
命名规范
- 自定义 Hooks 必须以
use开头 - 名称应清晰表达功能,如
useFetch、useForm、useDebounce
四、性能优化:useMemo 与 useCallback
何时使用
// ✅ 需要:计算密集型或引用稳定性
function ProductList({ products, filter }) {
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === filter);
}, [products, filter]);
const handleSelect = useCallback((id) => {
console.log('Selected:', id);
}, []);
return (
<div>
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} onSelect={handleSelect} />
))}
</div>
);
}
避免过度优化
// ❌ 不必要:简单计算
const doubled = useMemo(() => count * 2, [count]); // 直接计算即可
// ❌ 不必要:内联函数无子组件依赖
<button onClick={() => handleClick()}>点击</button>
原则: 只有当计算开销大或需要稳定引用传递给子组件时,才使用记忆化 Hooks。
五、常见陷阱与解决方案
闭包陷阱
// ❌ 错误:捕获旧值
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 始终是 0
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}
// ✅ 正确:使用函数式更新或添加依赖
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // 使用函数式更新
}, 1000);
return () => clearInterval(id);
}, []);
}
条件调用 Hooks
// ❌ 绝对禁止
if (condition) {
const [value, setValue] = useState(0);
}
// ✅ 正确:始终在顶层调用
const [value, setValue] = useState(0);
if (condition) {
// 条件逻辑放在这里
}
总结
React Hooks 是强大的工具,但需要正确使用才能发挥价值。记住以下核心原则:
- 依赖数组要完整——让 ESLint 帮你检查
- 避免派生状态——能计算就不要存储
- 提取自定义 Hooks——复用逻辑,保持组件简洁
- 谨慎优化——只在必要时使用 useMemo/useCallback
- 注意清理——异步操作和订阅必须清理
- 遵守规则——Hooks 只能在顶层调用
掌握这些实践,你的 React 代码将更加健壮、可维护。Hooks 不是银弹,但用对了,它们能让你的组件开发体验提升一个台阶。