React Hooks 完全指南:从入门到精通

366 阅读5分钟

引言

React Hooks 彻底改变了React组件的编写方式,让函数组件能够拥有类组件的全部功能。本文将基于实际项目代码,全面讲解React常用钩子函数的使用方法、最佳实践和常见陷阱,帮助你掌握React Hooks开发技巧。

什么是React Hooks?

Hooks是React 16.8引入的新特性,它们是一些可以让你在函数组件中使用状态和其他React特性的函数。Hooks的出现解决了类组件的几个主要问题:

  • 难以重用和共享组件逻辑
  • 复杂组件变得难以理解
  • 类组件中的this绑定问题

项目结构概览

我们的React项目结构如下:

react-hooks/
├── _mock/
│   └── _db.json       # 模拟数据库
├── public/
│   └── index.html     # 单页应用入口
├── src/
│   ├── App.jsx        # 主组件
│   ├── App2.jsx-App6.jsx  # 不同钩子函数示例
│   └── index.js       # 应用入口
└── README.md          # 项目文档

核心Hooks详解

一、 useState:状态管理基础

useState是最基本的钩子,用于在函数组件中添加状态。

基本用法

function App() {
  let [num, setNum] = useState(1); // [初始值, 更新函数]
  const [age, setAge] = useState(18);

  return (
    <div>
      <button onClick={() => setNum(num + 1)}>{num}</button>
      <h2 onClick={() => setAge(age + 1)}>{age}</h2>
    </div>
  );
}

注意事项

  • 不要直接修改状态,始终使用更新函数
  • 更新函数是异步的,不会立即反映在控制台输出中
  • 可以在一个组件中使用多个useState

二、 useEffect:处理副作用

useEffect让你可以在函数组件中执行副作用操作,如数据获取、订阅或手动修改DOM。

基本用法

useEffect(() => {
  console.log('useEffect');
  let timer = setInterval(() => {
    console.log(num);
  }, 1000);

  // 清理函数
  return () => {
    console.log('组件卸载');
    clearInterval(timer);
  };
}, [num]); // 依赖数组

依赖数组的作用

  • 空数组[]:只在组件挂载时执行一次
  • 包含变量[num]:组件挂载和num变化时执行
  • 不提供数组:每次渲染都执行

清理函数:用于清除副作用,如定时器、事件监听器等,防止内存泄漏。

三、 useLayoutEffect:同步执行副作用

useLayoutEffectuseEffect类似,但它会在浏览器绘制之前执行,适用于需要同步修改DOM的场景。

useLayoutEffect(() => {
  console.log('useLayoutEffect');
  queryData().then(data => {
    setNum(data);
  });
}, [num]);

useEffect vs useLayoutEffect

  • useEffect:异步执行,不会阻塞浏览器绘制
  • useLayoutEffect:同步执行,会阻塞浏览器绘制
  • 优先使用useEffect,只有在遇到布局问题时才使用useLayoutEffect

四、 useReducer:复杂状态管理

当组件内的状态逻辑变得复杂时,useReducer是比useState更好的选择。

基本用法

import { useReducer } from "react";
import { produce } from 'immer';

function reducer(state, action) {
  switch(action.type) {
    case 'add':
      return produce(state, (draft) => {
        draft.a.b.c += action.num;
      });
    case 'minus':
      return {
        ...state,
        result: state.result - action.num
      };
    default:
      return state;
  }
}

function App() {
  const [res, dispatch] = useReducer(reducer, {
    result: 0, 
    a: {b: {c: 1, d: {e: 2}}}
  });

  return (
    <div>
      <h3>{res.a.b.c}</h3>
      <button onClick={() => dispatch({type: 'add', num: 2})}>+</button>
      <button onClick={() => dispatch({type: 'minus', num: 1})}>-</button>
    </div>
  );
}

useReducer + Immer: 使用Immer库可以简化复杂状态的更新逻辑,允许直接"修改"状态(实际上是创建新状态)。

五、 useRef:访问DOM和保存可变值

useRef主要用于两种场景:访问DOM元素和保存不需要触发重渲染的可变值。

访问DOM元素

function App() {
  const ipt = useRef(null);

  useEffect(() => {
    ipt.current.focus(); // 自动聚焦输入框
  }, []);

  return (
    <div>
      <input type="text" ref={ipt} />
    </div>
  );
}

保存可变值: useRef创建的ref对象在组件的整个生命周期内保持不变,可以用来存储不需要引起重渲染的数据。

六、 useContext:跨组件数据传递

useContext提供了一种在组件树中共享数据的方式,无需手动逐层传递props。

使用步骤

  1. 创建Context
  2. 使用Provider提供数据
  3. 使用useContext消费数据
import { createContext, useContext } from "react";

// 创建Context
const numContext = createContext();

// 子组件使用Context
function Child2() {
  const count = useContext(numContext);
  return (
    <div>
      <h3>我是孙子组件 -- {count}</h3>
    </div>
  );
}

function Child1() {
  const num = useContext(numContext);
  return(
    <div>
      <h2>我是子组件 -- {num}</h2>
      <Child2 />
    </div>
  );
}

// 父组件提供Context
function App() {
  const num = 100;
  return (
    <div>
      <numContext.Provider value={num}>
        <h1>我是父组件</h1>
        <Child1 />
      </numContext.Provider>
    </div>
  );
}

适用场景

  • 主题切换
  • 用户登录状态
  • 多语言切换
  • 深层嵌套组件通信

七、 实际项目应用:数据管理与交互

App6.jsx展示了如何结合多个Hooks实现完整的数据管理功能,包括数据获取、搜索和删除:

function App() {
    const [dataSource, setDataSource] = useState([]);

    // 组件挂载时获取数据
    useEffect(() => {
        fetch("http://localhost:3001/data")
            .then(response => response.json())
            .then(data => setDataSource(data));
    }, []);

    // 搜索功能
    function onSearch(name) {
        fetch(`http://localhost:3001/data/?name=${name}`)
            .then(response => response.json())
            .then(data => setDataSource(data));
    }

    // 删除功能
    function onDelete(id) {
        fetch(`http://localhost:3001/data/${id}`, { method: 'DELETE' })
            .then(() => {
                const newData = dataSource.filter(item => item.id !== id);
                setDataSource(newData);
            });
    }

    return (
        <div className="container">
            <Search onSearch={onSearch} />
            <Table dataSource={dataSource} columns={columns} />
        </div>
    );
}

使用json-server搭建本地API

项目中使用json-server模拟后端API:

  1. 安装json-server:
npm install json-server --save-dev
  1. 在package.json中添加脚本:
"scripts": {
  "server": "json-server --watch _mock/_db.json --port 3001"
}
  1. 启动服务器:
npm run server
  1. 访问API:

页面效果图:

react-hooks.gif

八、 Hooks最佳实践

1. useState

  • 避免在循环、条件或嵌套函数中调用Hooks
  • 复杂状态考虑使用对象或数组
  • 状态更新可能是异步的,如需基于前一个状态计算新状态,使用函数形式:
    setNum(prevNum => prevNum + 1);
    

2.useEffect

  • 清理副作用防止内存泄漏
  • 正确设置依赖数组,避免遗漏依赖
  • 避免在useEffect中修改依赖项,防止无限循环

3. useReducer

  • 状态逻辑复杂时使用,提高可读性
  • 配合Immer库简化深层状态更新
  • 始终返回新状态对象
  • 不要忘记default case

4. useContext

  • 不要过度使用Context,可能导致性能问题
  • 频繁变化的数据不适合放在Context中
  • 可以创建多个Context,而不是一个包含所有状态的大Context

九、 总结

React Hooks为函数组件提供了强大的状态管理和生命周期功能,使代码更简洁、更易于理解和维护。本文介绍了六个核心Hooks的使用方法和最佳实践,并通过实际项目展示了如何在应用中使用这些Hooks。

掌握Hooks需要不断实践,关键是理解每个Hook的适用场景和注意事项。合理使用Hooks可以显著提高React应用的质量和开发效率。

扩展学习

  • React官方文档中的Hooks部分
  • 自定义Hook开发
  • React性能优化技巧
  • React 18新特性与Hooks