React Hooks 最佳实践指南

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 后数据不更新。

解决方案

  1. 安装 eslint-plugin-react-hooks,让它帮你检查
  2. 如果 ESLint 警告了,别忽略,它大概率是对的
  3. 如果真的不想添加某个依赖,用 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:

  1. 传递给子组件(子组件用 React.memo 优化过)
  2. 作为其他 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

核心原则

  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 使用规范。

安装方式

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。

安装方式

主要功能

  • 查看组件树和 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,我有三点最深的体会:

  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 打交道。

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

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

相关推荐
AAA不会前端开发1 小时前
从零手写 React:深度解析 Fiber 架构与 Hooks 实现
react.js·前端框架
敲代码的约德尔人2 小时前
React useEffect 完全指南:我在 3 个项目中踩坑后总结的血泪经验
前端·react.js
小凡同志2 小时前
React 组件设计模式:从 HOC 到 Render Props 再到 Hooks
前端·react.js
栀秋6662 小时前
深入浅出:手写一个迷你版 Zustand
前端·react.js·前端框架
GISer_Jing2 小时前
React核心语法:组件化与声明式编程
前端·react.js·前端框架
Alan Lu Pop2 小时前
React 表单提交关键词意外触发刷新
前端·javascript·react.js
我命由我123453 小时前
React - 创建 React 项目、React 项目结构、React 简单案例、TodoList 案例
前端·javascript·react.js·前端框架·ecmascript·html5·js
大雷神3 小时前
HarmonyOS APP<玩转React>开源教程二十二:每日一题功能
前端·react.js·开源·harmonyos
问道飞鱼3 小时前
【前端知识】使用React+Vite构建企业级项目模板
前端·react.js·前端框架·vite