React Hooks 完全指南:从概念到内置 Hooks 全解析
作为 React 16.8 引入的革命性特性,Hooks 彻底改变了 React 组件的编写方式。它让函数组件拥有了类组件的全部能力,同时解决了类组件的诸多痛点。本文将从 Hooks 的核心概念讲起,详细解析 React 内置的常用 Hooks,帮助你全面掌握这一重要特性。
一、什么是 React Hooks?
React Hooks(钩子)是一系列特殊的函数,它们允许你在函数组件中使用状态(State)、生命周期特性和其他 React 功能,而无需编写类组件。
Hooks 诞生的背景
在 Hooks 出现之前,函数组件被称为"无状态组件",只能接收 props 并返回 UI。复杂逻辑必须使用类组件,但类组件存在明显缺陷:
- 逻辑复用困难:为了复用状态逻辑,不得不使用高阶组件(HOC)、Render Props 等模式,容易形成"嵌套地狱"
- 生命周期混乱:一个生命周期方法(如
componentDidMount
)中常常混杂着不相关的逻辑(如数据请求、事件监听) - this 指向问题:类组件中
this
的绑定规则复杂,容易出现指向错误 - 学习成本高:理解类组件的继承、上下文等概念对新手不够友好
Hooks 正是为解决这些问题而生,它让开发者可以用更简洁的方式编写组件,同时保持逻辑的清晰与可复用性。
Hooks 的核心规则
使用 Hooks 必须遵守两条核心规则(React 会通过 ESLint 插件 eslint-plugin-react-hooks
自动校验):
-
只能在函数组件的顶层调用
不能在条件语句、循环、嵌套函数中调用 Hooks(确保 Hooks 的调用顺序在每次渲染时保持一致)。// 错误示例:在条件中调用 Hook function MyComponent() {if (someCondition) {const [count, setCount] = useState(0); // ❌ 禁止} }
-
只能在 React 函数组件或自定义 Hooks 中调用
不能在普通 JavaScript 函数中使用 Hooks。
二、React 内置核心 Hooks 详解
React 提供了多个内置 Hooks,每个都有特定的用途。下面我们逐一解析最常用的几个:
1. useState
:管理组件状态
useState
是最基础也最常用的 Hook,它让函数组件拥有了状态管理能力。
基本用法
import { useState } from 'react';function Counter() {// 声明状态变量:[当前值, 更新函数] = useState(初始值)const [count, setCount] = useState(0);return (<div><p>当前计数:{count}</p><button onClick={() => setCount(count + 1)}>加 1</button><button onClick={() => setCount(0)}>重置</button></div>);
}
核心特性
- 状态初始化:
useState(initialValue)
的参数为初始状态,可以是任意类型(数字、字符串、对象、数组等) - 状态更新:
setCount
是更新函数,调用后会触发组件重新渲染- 直接传值:
setCount(10)
(适用于不依赖当前状态的更新) - 函数传值:
setCount(prev => prev + 1)
(适用于依赖当前状态的更新,确保获取最新值)
- 直接传值:
- 状态独立性:每个
useState
声明的状态相互独立,多次调用可管理多个状态
复杂状态处理
useState
不仅能管理简单类型,还能处理对象和数组:
// 管理对象状态
const [user, setUser] = useState({ name: '张三', age: 20 });
// 更新对象(需创建新对象,避免直接修改原状态)
setUser(prev => ({ ...prev, age: prev.age + 1 }));// 管理数组状态
const [todos, setTodos] = useState(['学习 Hooks']);
// 添加数组元素
setTodos(prev => [...prev, '掌握 useState']);
2. useEffect
:处理副作用
useEffect
用于处理组件中的副作用(指与组件渲染无关的操作,如数据请求、事件监听、DOM 操作等),相当于类组件中 componentDidMount
、componentDidUpdate
和 componentWillUnmount
的结合体。
基本用法
import { useState, useEffect } from 'react';function UserProfile({ userId }) {const [user, setUser] = useState(null);// 副作用函数:处理数据请求useEffect(() => {// 定义异步请求函数const fetchUser = async () => {const response = await fetch(`/api/users/${userId}`);const data = await response.json();setUser(data);};fetchUser(); // 执行请求// 清理函数:组件卸载或依赖变化时执行return () => {console.log('组件卸载或 userId 变化,清理资源');// 实际开发中可用于取消请求、移除事件监听等};}, [userId]); // 依赖数组:仅当 userId 变化时重新执行if (!user) return <p>加载中...</p>;return <div>用户名:{user.name}</div>;
}
依赖数组的作用
useEffect
的第二个参数(依赖数组)决定了副作用函数的执行时机:
- 空数组
[]
:副作用函数仅在组件首次渲染后执行一次(类似componentDidMount
) - 包含依赖项
[a, b]
:副作用函数在首次渲染后和依赖项变化时执行(类似componentDidMount + componentDidUpdate
) - 无依赖数组:副作用函数在每次渲染后都执行
常见使用场景
- 数据请求(从 API 获取数据)
- 事件监听(如
window.resize
、scroll
) - DOM 操作(如初始化第三方库)
- 清理资源(如取消订阅、清除定时器)
3. useContext
:跨组件共享数据
useContext
用于在函数组件中获取 React Context 的值,避免了"props drilling"(props 层层传递)的问题,让跨组件数据共享更简洁。
基本用法
import { createContext, useContext } from 'react';// 1. 创建上下文(通常单独放在一个文件中)
const ThemeContext = createContext('light');// 2. 子组件:使用 useContext 获取上下文
function ThemedButton() {// 直接获取上下文值,无需通过 props 传递const theme = useContext(ThemeContext);return (<button style={{ background: theme === 'dark' ? '#333' : '#fff',color: theme === 'dark' ? '#fff' : '#333'}}>主题按钮</button>);
}// 3. 父组件:提供上下文值
function App() {return (<ThemeContext.Provider value="dark"><div><ThemedButton /> {/* 按钮会应用 dark 主题 */}</div></ThemeContext.Provider>);
}
注意事项
- 当
ThemeContext.Provider
的value
变化时,所有使用useContext(ThemeContext)
的组件都会重新渲染 - 通常会将
createContext
和useContext
配合使用,前者创建上下文,后者消费上下文 - 可以嵌套多个 Context 实现不同维度的数据共享
4. useReducer
:复杂状态管理
useReducer
适用于管理复杂状态逻辑,当状态更新依赖于先前状态、包含多个子值或需要统一的状态更新逻辑时,它比 useState
更合适(类似 Redux 的思想)。
基本用法
import { useReducer } from 'react';// 1. 定义 reducer 函数:接收当前状态和 action,返回新状态
function todoReducer(state, action) {switch (action.type) {case 'ADD_TODO':return [...state, { id: Date.now(), text: action.payload, done: false }];case 'TOGGLE_TODO':return state.map(todo => todo.id === action.payload ? { ...todo, done: !todo.done } : todo);case 'DELETE_TODO':return state.filter(todo => todo.id !== action.payload);default:return state;}
}// 2. 在组件中使用 useReducer
function TodoList() {// [当前状态, dispatch函数] = useReducer(reducer, 初始状态)const [todos, dispatch] = useReducer(todoReducer, []);const [inputText, setInputText] = useState('');const handleAdd = () => {if (!inputText.trim()) return;// 触发状态更新:通过 dispatch 发送 actiondispatch({ type: 'ADD_TODO', payload: inputText });setInputText('');};return (<div><input value={inputText} onChange={(e) => setInputText(e.target.value)} placeholder="请输入待办事项"/><button onClick={handleAdd}>添加</button><ul>{todos.map(todo => (<li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>{todo.text}<button onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}>{todo.done ? '取消完成' : '标记完成'}</button><button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>删除</button></li>))}</ul></div>);
}
优势分析
- 逻辑集中:所有状态更新逻辑集中在
reducer
中,便于维护 - 可预测性:通过
action
类型明确状态变更意图,使状态变化可追踪 - 处理复杂依赖:对于多个相互关联的状态,
useReducer
比多个useState
更高效
5. useRef
:持久化引用
useRef
用于创建一个持久化的引用容器,可以存储任意值,且修改它不会触发组件重新渲染。主要用于访问 DOM 元素或存储不需要参与渲染的数据。
基本用法
import { useRef, useState } from 'react';function TextInputWithFocus() {// 创建 ref 对象,初始值为 nullconst inputRef = useRef(null);const [value, setValue] = useState('');// 聚焦输入框const focusInput = () => {// 通过 ref.current 访问 DOM 元素inputRef.current.focus();};// 存储上一次输入的值(不触发渲染)const prevValueRef = useRef('');useEffect(() => {prevValueRef.current = value; // 修改 ref 值不会触发渲染}, [value]);return (<div><inputref={inputRef} // 将 ref 绑定到 DOM 元素value={value}onChange={(e) => setValue(e.target.value)}placeholder="输入内容..."/><button onClick={focusInput}>聚焦输入框</button><p>上一次输入:{prevValueRef.current}</p></div>);
}
主要用途
- 访问 DOM 元素:如获取输入框焦点、读取元素尺寸等
- 存储不需要触发渲染的数据:如定时器 ID、上一次的状态值等
- 在多次渲染间共享数据:ref 的值在组件生命周期内保持不变
6. 其他实用 Hooks
除了上述核心 Hooks,React 还提供了一些用于特定场景的 Hooks:
useMemo
:缓存计算结果
用于缓存 expensive 计算(耗时的计算),避免每次渲染都重复计算:
import { useMemo } from 'react';function ExpensiveComponent({ a, b }) {// 仅当 a 或 b 变化时,才重新计算 sumconst sum = useMemo(() => {console.log('计算 sum...');return a + b; // 假设这是一个耗时计算}, [a, b]); // 依赖数组return <p>Sum: {sum}</p>;
}
useCallback
:缓存函数引用
用于缓存函数,避免子组件因函数引用变化而不必要地重新渲染:
import { useCallback } from 'react';function ParentComponent() {const [count, setCount] = useState(0);// 仅当 count 变化时,才创建新的函数引用const handleClick = useCallback(() => {console.log('点击了,count:', count);}, [count]); // 依赖数组return <ChildComponent onClick={handleClick} />;
}
useLayoutEffect
:同步执行副作用
与 useEffect
类似,但会在 DOM 更新同步执行(而 useEffect
是异步的),适用于需要立即获取 DOM 状态的场景:
useLayoutEffect(() => {// 在 DOM 更新后立即执行(同步)console.log('DOM 已更新,可获取最新尺寸');
}, []);
三、Hooks 最佳实践
-
按功能拆分 Hooks
一个组件中可以使用多个 Hooks,按功能拆分(如一个useState
管理一个状态),保持逻辑清晰。 -
提取自定义 Hooks 复用逻辑
当多个组件需要共享逻辑时,将逻辑提取到自定义 Hooks 中:// 自定义 Hook:复用表单处理逻辑 function useForm(initialValues) {const [values, setValues] = useState(initialValues);const handleChange = (e) => {setValues(prev => ({ ...prev, [e.target.name]: e.target.value }));};return [values, handleChange]; }// 在组件中使用 function LoginForm() {const [form, handleChange] = useForm({ username: '', password: '' });// ... }
-
避免过度使用
useEffect
并非所有操作都需要放在useEffect
中,能在渲染过程中处理的逻辑就不要放入副作用。 -
正确设置依赖数组
确保useEffect
、useMemo
、useCallback
的依赖数组包含所有用到的外部变量,避免闭包陷阱。
四、总结
React Hooks 是函数组件的灵魂,它让组件逻辑更清晰、复用更简单。本文介绍的 useState
、useEffect
、useContext
、useReducer
、useRef
等内置 Hooks 覆盖了绝大多数开发场景:
- 用
useState
管理简单状态 - 用
useEffect
处理副作用 - 用
useContext
实现跨组件数据共享 - 用
useReducer
管理复杂状态逻辑 - 用
useRef
访问 DOM 或存储持久化数据
掌握这些 Hooks 后,你会发现编写 React 组件变得前所未有的简洁和高效。 Hooks 的真正力量不仅在于单个 Hook 的功能,更在于它们的组合使用——通过合理搭配,能轻松应对各种复杂场景。
开始在项目中实践这些 Hooks 吧,你会逐渐体会到它们带来的便利!