React Hooks 最佳实践指南
从 Class 到 Function 的思维转变,掌握 Hooks 的核心用法和常见陷阱
前言
三年前,我第一次接触 React Hooks 时,心里是抗拒的。"好好的 Class 组件不用,为什么要用这种奇怪的语法?"------这是当时的真实想法。
直到有一天,我在一个大型后台项目中遇到了这个问题:
javascript
// 这是三年前的我写的 Class 组件
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = { user: null, loading: true, error: null };
this.handleResize = this.handleResize.bind(this); // 又要 bind this
}
componentDidMount() {
fetchUser(this.props.userId).then(...);
window.addEventListener('resize', this.handleResize); // 记得订阅
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize); // 记得清理
}
// 还有各种生命周期方法...
}
代码越来越多,我发现自己经常忘记 bind this,忘记在 componentWillUnmount 中清理副作用,忘记... 总之,各种忘记。
后来我尝试了 Hooks,真香了。
这篇文章不是官方文档的翻译,而是我在 3 个大型项目中使用 Hooks 两年的经验总结。我会告诉你:
- Hooks 最常见的 5 个陷阱(我都踩过)
- 如何避免不必要的重渲染(性能提升 50%+)
- 自定义 Hook 的正确打开方式(逻辑复用神器)
- 哪些场景不应该用 Hooks(别盲目跟风)
如果你也在使用 Hooks 时遇到过"为什么 effect 总是执行两次?"、"为什么状态更新没有生效?"这类问题,这篇文章就是为你写的。
一、Hooks 核心概念
什么是 Hooks?
简单说,Hooks 让你在不写 class 的情况下使用 state 和其他 React 特性。
但我更喜欢这样理解:Hooks 是把原本分散在生命周期方法中的代码,按照"逻辑相关性"重新组织的方式。
javascript
// Class 组件:相关逻辑被拆散在不同生命周期中
class Example extends React.Component {
componentDidMount() {
// 数据获取
fetchData();
// 事件监听
window.addEventListener('resize', handleResize);
}
componentDidUpdate() {
// 数据获取(又要写一遍)
fetchData();
}
componentWillUnmount() {
// 清理(别忘了!)
window.removeEventListener('resize', handleResize);
}
}
// Hooks:相关逻辑放在一起
function Example() {
// 数据获取逻辑
useEffect(() => {
fetchData();
}, [deps]);
// 事件监听逻辑
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
}
看到了吗?Hooks 让相关的代码在一起 ,而不是相同生命周期的代码在一起。
内置 Hooks 分类
| 类型 | Hook | 用途 | 使用频率 |
|---|---|---|---|
| 状态类 | useState | 管理组件状态 | ⭐⭐⭐⭐⭐ |
| 副作用类 | useEffect | 处理副作用 | ⭐⭐⭐⭐⭐ |
| 上下文类 | useContext | 消费 Context 值 | ⭐⭐⭐ |
| 性能类 | useMemo | 记忆化计算结果 | ⭐⭐⭐ |
| 性能类 | useCallback | 记忆化回调函数 | ⭐⭐ |
| 引用类 | useRef | 访问 DOM 或保存可变值 | ⭐⭐⭐⭐ |
我的建议:先精通 useState 和 useEffect,这两个占了日常开发的 80%。其他 Hook 等遇到具体场景再学。
二、常见陷阱与解决方案
陷阱一:useEffect 依赖项缺失(我踩过最多的坑)
javascript
// ❌ 我当年这样写过,bug 找了我一天
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(() => {
fetchUser(userId).then(setUser);
}, [userId]); // 完整依赖
return <div>{user?.name}</div>;
}
问题表现:页面显示的用户信息永远是第一次加载的,切换 userId 后数据不更新。
解决方案:
- 安装
eslint-plugin-react-hooks,让它帮你检查 - 如果 ESLint 警告了,别忽略,它大概率是对的
- 如果真的不想添加某个依赖,用
useRef保存它
陷阱二:useState 初始值计算开销大
javascript
// ❌ 每次渲染都执行计算(性能杀手)
function TodoList({ todos }) {
const [count, setCount] = useState(computeExpensiveValue(todos));
// computeExpensiveValue 每次渲染都会调用!
return <div>{count}</div>;
}
// ✅ 使用惰性初始化(只执行一次)
function TodoList({ todos }) {
const [count, setCount] = useState(() => computeExpensiveValue(todos));
// 传入函数,只在首次渲染时执行
return <div>{count}</div>;
}
性能对比:在一个有 1000+ todo 的项目中,这个优化让首屏渲染时间从 800ms 降到 200ms。
陷阱三:useCallback 滥用(我也犯过的错)
javascript
// ❌ 我曾经给每个函数都包 useCallback
function MyComponent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
const handleChange = useCallback((e) => {
console.log(e.target.value);
}, []);
const handleSubmit = useCallback(() => {
console.log('submitted');
}, []);
// 代码看起来很"优化",实际没必要
}
// ✅ 简单场景不需要 useCallback
function MyComponent() {
const handleClick = () => {
console.log('clicked');
};
const handleChange = (e) => {
console.log(e.target.value);
};
// 代码更简洁,性能也没差
}
原则:只有在以下情况才需要 useCallback:
- 传递给子组件(子组件用 React.memo 优化过)
- 作为其他 Hook 的依赖(比如 useEffect 的依赖数组)
别过早优化------这是我从血泪中学到的教训。
陷阱四:useMemo 依赖不完整
javascript
// ❌ 依赖不完整,缓存会失效
const filtered = useMemo(() => {
return items.filter(item => item.category === category);
}, [items]); // 缺少 category
// ✅ 完整依赖
const filtered = useMemo(() => {
return items.filter(item => item.category === category);
}, [items, category]);
检查方法:问自己"这个变量的变化会影响计算结果吗?"如果会,就加入依赖。
陷阱五:在循环或条件语句中使用 Hooks
javascript
// ❌ 这是禁止的!
function MyComponent({ count }) {
for (let i = 0; i < count; i++) {
const [value, setValue] = useState(0); // ❌ Hooks 不能在循环中调用
}
if (someCondition) {
useEffect(() => {}); // ❌ Hooks 不能在条件语句中调用
}
}
原因:Hooks 依赖调用顺序来关联状态,顺序变化会导致状态错乱。
三、性能优化技巧
1. React.memo 配合 useMemo
javascript
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 保持对象引用稳定,避免子组件不必要的重渲染
const config = useMemo(() => ({ theme: 'dark' }), []);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<Child config={config} />
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
</div>
);
}
const Child = React.memo(function Child({ config }) {
console.log('Child rendered');
return <div>Theme: {config.theme}</div>;
});
效果:当 count 变化时,Child 组件不会重渲染(因为 config 引用稳定)。
2. 使用自定义 Hook 封装逻辑
javascript
// 这是我用得最多的自定义 Hook 之一
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return width;
}
// 使用
function MyComponent() {
const width = useWindowWidth();
return <div>窗口宽度:{width}</div>;
}
好处:
- 逻辑可复用(多个组件都能用)
- 测试方便(单独测试 Hook)
- 组件更简洁(只关心 UI)
3. 使用 useReducer 管理复杂状态
javascript
// 当 useState 不够用时,上 useReducer
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</>
);
}
使用场景:
- 状态有多个子值
- 下一个状态依赖之前的状态
- 状态更新逻辑复杂
四、实战场景
场景 1:表单处理
背景:在一个后台管理系统中,我需要处理一个有 20+ 字段的表单。最初我为每个字段创建了独立的状态和处理函数,代码惨不忍睹。
javascript
// ❌ 我最初的写法(别学)
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [address, setAddress] = useState('');
// ... 还有 16 个字段
function handleNameChange(e) { setName(e.target.value); }
function handleEmailChange(e) { setEmail(e.target.value); }
function handlePhoneChange(e) { setPhone(e.target.value); }
// ... 还有 16 个处理函数
}
// ✅ 重构后的写法
function Form() {
const [values, setValues] = useState({
name: '',
email: '',
phone: '',
address: '',
// ... 其他字段
});
function handleChange(field, value) {
setValues(prev => ({ ...prev, [field]: value }));
}
return (
<>
<input
value={values.name}
onChange={e => handleChange('name', e.target.value)}
/>
<input
value={values.email}
onChange={e => handleChange('email', e.target.value)}
/>
{/* 其他字段... */}
</>
);
}
效果:代码量减少 70%,新增字段只需要在初始状态中添加,不需要写新的处理函数。
场景 2:数据获取(处理竞态条件)
背景:在一个文章详情页,用户可能快速切换不同文章。如果网络请求返回顺序和发起顺序不一致,会显示错误的数据。
javascript
function Article({ id }) {
const [article, setArticle] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let ignore = false; // 关键:忽略标志
fetchArticle(id).then(data => {
if (!ignore) {
setArticle(data);
setLoading(false);
}
});
return () => {
ignore = true; // 组件卸载或 id 变化时,忽略之前的请求
};
}, [id]);
if (loading) return <div>加载中...</div>;
return <div>{article.title}</div>;
}
效果:即使用户快速切换文章,也不会出现"闪回"问题(旧数据覆盖新数据)。
场景 3:防抖搜索
背景:搜索输入需要防抖处理,避免每次输入都触发 API 请求。
javascript
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
const timer = setTimeout(() => {
if (query) {
search(query).then(setResults);
}
}, 300); // 300ms 防抖
return () => {
clearTimeout(timer); // 清理定时器
};
}, [query]);
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
);
}
效果:用户停止输入 300ms 后才发起请求,API 调用次数减少 80%+。
五、最佳实践总结
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 简单状态 | useState | 直接使用 |
| 复杂状态 | useReducer | 状态逻辑集中管理 |
| 副作用 | useEffect | 记得清理 |
| 依赖数组 | 完整依赖 | 使用 ESLint 检查 |
| 回调函数 | useCallback | 仅在必要时 |
| 计算结果 | useMemo | 避免重复计算 |
| DOM 引用 | useRef | 不触发重渲染 |
| 组件优化 | React.memo | 配合稳定 props |
核心原则
- 每个 Hook 关注单一职责 - 不要把所有逻辑塞进一个 useEffect
- 依赖数组必须完整 - 使用 ESLint 插件自动检查
- 及时清理副作用 - 尤其是定时器和订阅
- 避免过早优化 - 先保证正确,再考虑性能
- 封装可复用逻辑 - 使用自定义 Hook
我的个人建议
经过两年的 Hooks 使用经验,我有以下几点建议:
- 不要迷信性能优化 - 90% 的场景不需要 useMemo/useCallback
- ESLint 是你的朋友 - 它帮你避免 99% 的 Hooks 陷阱
- 自定义 Hook 是神器 - 把可复用逻辑抽离出来,代码会清爽很多
- 多看看别人的代码 - GitHub 上有很多优秀的开源项目可以学习
六、工具推荐
1. ESLint 插件(必装)
eslint-plugin-react-hooks 是 React 官方提供的 ESLint 插件,自动检查 Hooks 使用规范。
安装方式:
bash
npm install -D eslint-plugin-react-hooks
配置 .eslintrc.js:
javascript
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
我的体验:这个插件帮我避免了至少 20+ 个潜在 bug,强烈建议每个 React 项目都装上。
2. React DevTools(必装)
React 官方浏览器扩展,支持 Chrome、Firefox、Edge。
安装方式:
- Chrome: chrome.google.com/webstore/de...
- Firefox: addons.mozilla.org/firefox/add...
主要功能:
- 查看组件树和 Hook 状态
- 分析重渲染原因
- 调试自定义 Hook
- 性能分析(Profiling)
使用技巧:打开"Highlight Updates"功能,可以直观看到哪些组件在重渲染。
3. why-did-you-render(性能优化时用)
检测不必要的重渲染,帮助定位性能问题。
安装方式:
bash
npm install @welldone-software/why-did-you-render
配置示例:
javascript
import whyDidYouRender from '@welldone-software/why-did-you-render';
import React from 'react';
whyDidYouRender(React, {
trackAllPureComponents: true,
});
适用场景:当你发现页面卡顿,但不知道是哪个组件导致的时候。
总结
写了两年的 Hooks,我有三点最深的体会:
- Hooks 让代码更简洁 - 同样的功能,Hooks 代码量通常是 Class 的一半
- Hooks 让逻辑更清晰 - 相关代码在一起,不再分散在各个生命周期方法中
- Hooks 需要思维转变 - 从"生命周期"思维转向"效果"思维
如果你刚开始学 Hooks,我的建议是:
- 先掌握 useState 和 useEffect,这两个够用 80% 的场景
- 装上 ESLint 插件,让它帮你检查错误
- 多写多练,踩几个坑就学会了
- 遇到问题先看官方文档,React 的文档质量很高
最后,别被各种"最佳实践"吓到。先写出能跑的代码,再优化------这是我从无数个项目中学到的真理。
参考资料
- React 官方文档 - Hooks 介绍:react.dev/reference/r...
- React 官方文档 - useState: react.dev/reference/r...
- React 官方文档 - useEffect: react.dev/reference/r...
- React 官方文档 - 自定义 Hook: react.dev/learn/reusi...
- patterns.dev - React Hooks 模式:www.patterns.dev/react/hooks
觉得文章对你有帮助?
- 👍 点赞支持一下,让我更有动力创作
- ⭐ 收藏备用,下次遇到类似问题快速找到
- 📢 分享给团队伙伴,一起提升代码质量
- 💬 评论区聊聊:你在使用 Hooks 时遇到过哪些坑?
你的每一次互动,都是我继续创作的动力!
关于作者
前端开发 8 年,踩过无数坑,写过不少烂代码。现在在一家创业公司负责前端架构,日常和 React/Vue 打交道。
我的目标:分享最实用的前端技巧,帮助大家少踩坑,早点下班摸鱼🐟。
关注我,获取更多前端实战内容!