React Hooks 最佳实践指南

4 阅读10分钟

React Hooks 最佳实践指南

从 Class 到 Function 的思维转变,掌握 Hooks 的核心用法和常见陷阱


前言

三年前,我第一次接触 React Hooks 时,心里是抗拒的。"好好的 Class 组件不用,为什么要用这种奇怪的语法?"——这是当时的真实想法。

直到有一天,我在一个大型后台项目中遇到了这个问题:

// 这是三年前的我写的 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 是把原本分散在生命周期方法中的代码,按照"逻辑相关性"重新组织的方式。

// 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 依赖项缺失(我踩过最多的坑)

// ❌ 我当年这样写过,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 后数据不更新。

解决方案

  1. 安装 eslint-plugin-react-hooks,让它帮你检查
  2. 如果 ESLint 警告了,别忽略,它大概率是对的
  3. 如果真的不想添加某个依赖,用 useRef 保存它

陷阱二:useState 初始值计算开销大

// ❌ 每次渲染都执行计算(性能杀手)
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 滥用(我也犯过的错)

// ❌ 我曾经给每个函数都包 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:

  1. 传递给子组件(子组件用 React.memo 优化过)
  2. 作为其他 Hook 的依赖(比如 useEffect 的依赖数组)

别过早优化——这是我从血泪中学到的教训。

陷阱四:useMemo 依赖不完整

// ❌ 依赖不完整,缓存会失效
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

// ❌ 这是禁止的!
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

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 封装逻辑

// 这是我用得最多的自定义 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 管理复杂状态

// 当 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+ 字段的表单。最初我为每个字段创建了独立的状态和处理函数,代码惨不忍睹。

// ❌ 我最初的写法(别学)
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:数据获取(处理竞态条件)

背景:在一个文章详情页,用户可能快速切换不同文章。如果网络请求返回顺序和发起顺序不一致,会显示错误的数据。

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 请求。

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

核心原则

  1. 每个 Hook 关注单一职责 - 不要把所有逻辑塞进一个 useEffect
  2. 依赖数组必须完整 - 使用 ESLint 插件自动检查
  3. 及时清理副作用 - 尤其是定时器和订阅
  4. 避免过早优化 - 先保证正确,再考虑性能
  5. 封装可复用逻辑 - 使用自定义 Hook

我的个人建议

经过两年的 Hooks 使用经验,我有以下几点建议:

  1. 不要迷信性能优化 - 90% 的场景不需要 useMemo/useCallback
  2. ESLint 是你的朋友 - 它帮你避免 99% 的 Hooks 陷阱
  3. 自定义 Hook 是神器 - 把可复用逻辑抽离出来,代码会清爽很多
  4. 多看看别人的代码 - GitHub 上有很多优秀的开源项目可以学习

六、工具推荐

1. ESLint 插件(必装)

eslint-plugin-react-hooks 是 React 官方提供的 ESLint 插件,自动检查 Hooks 使用规范。

安装方式

npm install -D eslint-plugin-react-hooks

配置 .eslintrc.js

{
  "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。

安装方式

主要功能

  • 查看组件树和 Hook 状态
  • 分析重渲染原因
  • 调试自定义 Hook
  • 性能分析(Profiling)

使用技巧:打开"Highlight Updates"功能,可以直观看到哪些组件在重渲染。

3. why-did-you-render(性能优化时用)

检测不必要的重渲染,帮助定位性能问题。

安装方式

npm install @welldone-software/why-did-you-render

配置示例

import whyDidYouRender from '@welldone-software/why-did-you-render';
import React from 'react';

whyDidYouRender(React, {
  trackAllPureComponents: true,
});

适用场景:当你发现页面卡顿,但不知道是哪个组件导致的时候。


总结

写了两年的 Hooks,我有三点最深的体会:

  1. Hooks 让代码更简洁 - 同样的功能,Hooks 代码量通常是 Class 的一半
  2. Hooks 让逻辑更清晰 - 相关代码在一起,不再分散在各个生命周期方法中
  3. Hooks 需要思维转变 - 从"生命周期"思维转向"效果"思维

如果你刚开始学 Hooks,我的建议是:

  • 先掌握 useState 和 useEffect,这两个够用 80% 的场景
  • 装上 ESLint 插件,让它帮你检查错误
  • 多写多练,踩几个坑就学会了
  • 遇到问题先看官方文档,React 的文档质量很高

最后,别被各种"最佳实践"吓到。先写出能跑的代码,再优化——这是我从无数个项目中学到的真理。


参考资料

  1. React 官方文档 - Hooks 介绍:react.dev/reference/r…
  2. React 官方文档 - useState: react.dev/reference/r…
  3. React 官方文档 - useEffect: react.dev/reference/r…
  4. React 官方文档 - 自定义 Hook: react.dev/learn/reusi…
  5. patterns.dev - React Hooks 模式:www.patterns.dev/react/hooks

觉得文章对你有帮助?

  • 👍 点赞支持一下,让我更有动力创作
  • 收藏备用,下次遇到类似问题快速找到
  • 📢 分享给团队伙伴,一起提升代码质量
  • 💬 评论区聊聊:你在使用 Hooks 时遇到过哪些坑?

你的每一次互动,都是我继续创作的动力!


关于作者

前端开发 8 年,踩过无数坑,写过不少烂代码。现在在一家创业公司负责前端架构,日常和 React/Vue 打交道。

我的目标:分享最实用的前端技巧,帮助大家少踩坑,早点下班摸鱼🐟。

关注我,获取更多前端实战内容!