React Hooks 从入门到实战:一篇搞懂 React 常用 Hook
适合人群:有 HTML、CSS、JavaScript 基础,刚开始学习 React 的后端开发者。
学习目标:理解 Hook 的作用,掌握常用 Hook,能够看懂并编写 React 业务组件。
一、什么是 React Hook?
1.1 先理解 React 组件
在 React 中,页面通常由一个个组件组成。
比如下面是一个最简单的 React 组件:
javascript
function App() {
return <h1>Hello React</h1>;
}
export default App;
这里需要理解三个知识点:
App:一个普通的 JavaScript 函数return:返回需要渲染的页面内容JSX:允许在 JavaScript 中编写类似 HTML 的语法
我们可以简单理解为:
React 组件 = 一个返回页面内容的函数。
但是,这种普通函数组件存在一个问题:怎么管理会变化的数据?
1.2 普通变量为什么不够用?
假设我们需要实现一个计数器。
javascript
function Counter() {
let count = 0;
function add() {
count++;
console.log(count);
}
return (
<div>
<p>当前数字:{count}</p>
<button onClick={add}>+1</button>
</div>
);
}
点击按钮后:
count确实增加了。console.log可以看到变化。- 但是页面上的数字不会因此自动更新。
原因是:
React 不会因为普通局部变量发生变化,就重新渲染组件。
而且组件重新渲染时,函数会重新执行,普通局部变量也会重新初始化。
所以 React 提供了一种管理状态的机制:Hook。
1.3 Hook 到底是什么?
Hook 可以理解为:
React 提供的一组特殊函数,让函数组件拥有状态管理、副作用处理、引用保存等能力。
例如:
scss
const [count, setCount] = useState(0);
这里的 useState 就是一个 Hook。
Hook 的名字通常以 use 开头。
常见 Hook:
| Hook | 主要作用 | 通俗理解 |
|---|---|---|
useState |
管理组件状态 | 保存会变化的数据 |
useEffect |
处理副作用 | 数据变化后执行一些操作 |
useRef |
保存可变引用 | 保存值或操作 DOM |
useMemo |
缓存计算结果 | 避免重复做昂贵计算 |
useCallback |
缓存函数引用 | 避免每次都创建新函数 |
useContext |
跨组件共享数据 | 减少逐层传递 Props |
useReducer |
管理复杂状态 | 集中处理状态变化 |
刚入门时,优先掌握 useState 和 useEffect。
二、useState:管理组件状态(最重要)
2.1 基本语法
javascript
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前数字:{count}</p>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}
拆解:
scss
const [count, setCount] = useState(0);
相当于:
count:当前状态值setCount:修改状态的函数0:初始值
useState 返回一个数组,包含状态值和更新函数。
这里用到了 JavaScript 的数组解构语法。
例如:
ini
const arr = [10, 20];
const [a, b] = arr;
console.log(a); // 10
console.log(b); // 20
所以:
scss
const [count, setCount] = useState(0);
可以理解为:
React,帮我保存一个叫 count 的状态,初始值为 0,并提供一个修改它的方法 setCount。
当执行:
scss
setCount(10);
React 会安排组件重新渲染,让页面显示最新状态。
2.2 useState 的常见用法
用法一:数字状态
scss
const [count, setCount] = useState(0);
setCount(10);
setCount(prev => prev + 1);
其中:
ini
prev => prev + 1
表示根据上一次状态计算新状态。
当新状态依赖旧状态时,推荐这种写法。
用法二:字符串状态
ini
const [name, setName] = useState('');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
用户输入时,name 跟着发生变化。
这里:
value:输入框当前显示的值onChange:输入内容变化时触发e.target.value:获取用户输入的内容
用法三:布尔状态
经常用于控制弹窗、加载状态、显示与隐藏。
javascript
const [visible, setVisible] = useState(false);
return (
<div>
<button onClick={() => setVisible(!visible)}>
切换显示
</button>
{visible && <p>这是一段内容</p>}
</div>
);
这里的:
css
visible && <p>这是一段内容</p>
表示:
如果 visible 为 true,就渲染内容。
用法四:数组状态
例如一个任务列表:
ini
const [tasks, setTasks] = useState<string[]>([]);
function addTask() {
setTasks(prev => [...prev, '新任务']);
}
这里:
css
[...prev, '新任务']
表示创建一个新数组,把旧数组元素复制过来,再添加一个新元素。
为什么不直接使用 push?
arduino
tasks.push('新任务'); // 不推荐
因为 React 状态应该按不可变数据的方式更新。直接修改原数组不会主动通知 React 更新页面。
用法五:对象状态
php
const [user, setUser] = useState({
name: '张三',
age: 20
});
function updateAge() {
setUser(prev => ({
...prev,
age: 21
}));
}
这里的 ...prev 表示保留其他字段,只修改 age。
2.3 TypeScript 中的 useState
实际工作中,经常会看到:
typescript
interface User {
id: number;
name: string;
}
const [user, setUser] = useState<User | null>(null);
拆解:
sql
useState<User | null>(null)
User:指定状态的数据类型| null:允许当前没有用户null:初始化为空
这就是 TypeScript 的泛型与联合类型。
也可以这样写:
ini
const [list, setList] = useState<User[]>([]);
表示 list 是一个 User 对象数组。
2.4 useState 最容易踩的坑
scss
setCount(count + 1);
console.log(count);
执行之后,console.log 不一定打印更新后的状态。
原因:
调用 setCount 不会直接修改当前这次函数执行中 count 变量的值。
React 会在后续渲染中提供新的状态值。
另外,连续执行:
scss
setCount(count + 1);
setCount(count + 1);
不能保证增加两次,因为两次读取的可能是相同的 count。
正确写法:
ini
setCount(prev => prev + 1);
setCount(prev => prev + 1);
三、useEffect:处理副作用
3.1 什么叫副作用?
先来看一个场景。
页面打开时,需要请求后端接口获取任务列表。
在 React 中,渲染页面是组件的主要职责。
而下面这些行为不是单纯计算页面内容:
- 请求后端接口
- 注册浏览器事件
- 创建定时器
- 订阅外部消息
- 操作浏览器外部系统
这类操作通常称为副作用(Side Effect) 。
React 提供 useEffect,用于让组件与外部系统同步。
3.2 基本语法
javascript
import { useEffect } from 'react';
useEffect(() => {
console.log('Effect 执行了');
}, []);
拆解:
scss
useEffect(
() => {
// 需要执行的副作用
},
[]
);
它接收两个主要参数:
- 第一个参数:副作用执行函数
- 第二个参数:依赖数组
依赖数组决定 Effect 在哪些情况下需要重新执行。
3.3 useEffect 的三种常见写法
第一种:不传依赖数组
javascript
useEffect(() => {
console.log('组件渲染完成');
});
首次渲染后执行,并且每次重新渲染后都会执行。
第二种:空依赖数组
scss
useEffect(() => {
console.log('组件挂载');
}, []);
一般情况下,组件挂载后执行一次。
注意:开发环境启用 React Strict Mode 时,React 可能额外执行一次 Effect 的设置与清理,用于检查代码是否安全。因此开发时看到两次日志,并不一定是程序异常。
第三种:指定依赖变量
scss
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count 发生变化:', count);
}, [count]);
首次渲染后执行,并在 count 变化后重新执行。
注意:并不是只有变化时才执行,首次挂载后也会执行。
3.4 useEffect 请求接口
假设接口如下:
bash
GET /api/tasks
返回:
css
[ { "id": 1, "name": "任务A" }, { "id": 2, "name": "任务B" }]
React 中可以这样写:
ini
import { useState, useEffect } from 'react';
interface Task {
id: number;
name: string;
}
function TaskList() {
const [tasks, setTasks] = useState<Task[]>([]);
useEffect(() => {
async function fetchTasks() {
const response = await fetch('/api/tasks');
const data: Task[] = await response.json();
setTasks(data);
}
fetchTasks();
}, []);
return (
<div>
{tasks.map(task => (
<p key={task.id}>{task.name}</p>
))}
</div>
);
}
执行流程:
- 组件第一次渲染,
tasks为空数组。 - React 完成页面渲染。
useEffect执行,调用接口。- 接口返回数据。
setTasks(data)更新状态。- React 重新渲染,页面显示任务列表。
这就是最常见的 React 数据请求流程之一。
生产环境还应考虑请求错误、加载状态、请求取消等问题。
3.5 useEffect 的清理函数
假设监听浏览器窗口大小:
javascript
useEffect(() => {
function handleResize() {
console.log(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
这里:
javascript
return () => {
window.removeEventListener('resize', handleResize);
};
是清理函数。
清理函数会在组件卸载时执行,也会在 Effect 依赖变化、重新执行之前调用。
目的就是避免重复注册监听或资源泄漏。
3.6 useEffect 的常见误区
很多新手看到数据变化,就想用 useEffect。
例如:
scss
const [price, setPrice] = useState(100);
const [count, setCount] = useState(2);
const [total, setTotal] = useState(200);
useEffect(() => {
setTotal(price * count);
}, [price, count]);
这虽然能够工作,但是不推荐。
因为 total 完全可以由 price 和 count 计算出来。
更简单的写法:
ini
const [price, setPrice] = useState(100);
const [count, setCount] = useState(2);
const total = price * count;
能在渲染过程中直接计算的数据,通常不需要额外用 useEffect 和 useState 保存。
四、useRef:保存引用和操作 DOM
4.1 useRef 是什么?
先记住一个最重要的区别:
useState:修改状态通常会触发重新渲染useRef:修改.current不会触发重新渲染
基本语法:
ini
const countRef = useRef(0);
它会返回:
css
{
current: 0
}
可以通过:
ini
countRef.current = 10;
修改值。
4.2 useRef 操作 DOM
例如点击按钮后,自动聚焦输入框。
javascript
import { useRef } from 'react';
function Search() {
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus();
}
return (
<div>
<input ref={inputRef} />
<button onClick={focusInput}>
聚焦输入框
</button>
</div>
);
}
拆解:
ini
const inputRef = useRef<HTMLInputElement>(null);
表示创建一个引用,用于保存 HTML 输入框的 DOM 元素。
ini
<input ref={inputRef} />
告诉 React:将这个输入框的 DOM 元素赋给 inputRef.current。
ini
inputRef.current?.focus();
调用输入框的聚焦方法。
其中 ?. 是 JavaScript 的可选链操作符。
如果 current 不为 null,就执行 focus()。
4.3 useRef 保存定时器
javascript
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
function start() {
if (timerRef.current !== null) return;
timerRef.current = setInterval(() => {
console.log('执行定时任务');
}, 1000);
}
function stop() {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}
这里的 timerRef 用来保存定时器 ID。
因为保存定时器 ID 本身不需要刷新页面,所以适合使用 useRef。
五、useMemo:缓存计算结果
5.1 为什么需要 useMemo?
React 组件每次重新渲染时,函数中的普通计算也会重新执行。
例如:
javascript
function App() {
const [count, setCount] = useState(0);
const result = expensiveCalculation();
return <div>{result}</div>;
}
即使 expensiveCalculation() 的输入没有变化,点击按钮更新 count 后,组件重新渲染,也会重新计算。
如果计算非常耗时,就可能造成性能浪费。
5.2 useMemo 的基本语法
ini
const result = useMemo(() => {
return expensiveCalculation();
}, []);
它会缓存计算结果。
只有依赖发生变化时,才重新计算。
例如:
ini
const [list, setList] = useState([1, 2, 3]);
const [count, setCount] = useState(0);
const total = useMemo(() => {
console.log('重新计算总和');
return list.reduce((sum, num) => sum + num, 0);
}, [list]);
这里:
list变化时,重新计算totalcount变化时,通常复用缓存的total
注意:useMemo 是性能优化工具,不是保证只执行一次的机制。
5.3 什么时候使用 useMemo?
适用于:
- 大量数据排序
- 复杂列表过滤
- 大量数据聚合
- 昂贵的计算逻辑
例如:
ini
const filteredTasks = useMemo(() => {
return tasks.filter(task =>
task.name.includes(keyword)
);
}, [tasks, keyword]);
如果数据很少、计算很简单,直接计算通常就足够了。
不要为了使用 Hook 而使用 Hook。
六、useCallback:缓存函数
6.1 useCallback 是什么?
在 React 函数组件中:
javascript
function App() {
const handleClick = () => {
console.log('click');
};
return <button onClick={handleClick}>点击</button>;
}
每次组件重新渲染,都会创建一个新的 handleClick 函数对象。
通常这没有问题。
但是,如果将函数传给经过 memo 优化的子组件,新的函数引用可能导致子组件不必要地重新渲染。
6.2 基本语法
ini
const handleClick = useCallback(() => {
console.log('click');
}, []);
useCallback 会缓存函数引用。
只有依赖变化时,React 才会返回新的函数引用。
例如:
ini
const [count, setCount] = useState(0);
const handleAdd = useCallback(() => {
setCount(prev => prev + 1);
}, []);
这里依赖数组为空是合理的,因为函数只使用了稳定的 setCount 更新函数,并通过函数式更新读取最新状态。
6.3 useMemo 和 useCallback 的区别
| 对比 | useMemo | useCallback |
|---|---|---|
| 缓存什么 | 计算结果 | 函数引用 |
| 返回什么 | 计算后的值 | 函数本身 |
| 主要用途 | 减少昂贵计算 | 保持函数引用稳定 |
一句话记忆:
useMemo 缓存结果,useCallback 缓存函数。
不要默认给所有函数套 useCallback,只有确实需要稳定函数引用时再使用。
七、useContext:跨组件传递数据
7.1 为什么需要 useContext?
假设组件结构如下:
css
App
└── Layout
└── Header
└── UserInfo
现在 App 里面有一个用户名:
ini
const username = '张三';
如果使用 Props 传递,需要:
css
App → Layout → Header → UserInfo
中间每一层都需要接收并继续传递参数。
这种现象叫做 Props Drilling(属性逐层传递)。
React 提供 Context 来共享数据,useContext 则用于读取这些数据。
7.2 示例:共享用户信息
先创建 Context:
ini
import { createContext, useContext } from 'react';
const UserContext = createContext('游客');
父组件提供数据:
javascript
function App() {
return (
<UserContext.Provider value="张三">
<Header />
</UserContext.Provider>
);
}
子组件读取:
javascript
function Header() {
const username = useContext(UserContext);
return <h1>欢迎,{username}</h1>;
}
即使中间存在很多层组件,也可以通过 Context 读取最近的上层 Provider 提供的数据。
7.3 useContext 适合什么?
常见场景:
- 登录用户信息
- 主题配置
- 国际化语言配置
- 全局偏好设置
但是 Context 并不等于完整的状态管理框架。
共享值变化时,读取该 Context 的组件可能重新渲染,因此仍然需要关注使用范围和性能。
八、useReducer:复杂状态管理
8.1 为什么需要 useReducer?
如果一个页面存在多个状态:
scss
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [data, setData] = useState([]);
随着业务逻辑越来越复杂,状态之间会存在关联。
此时可以考虑 useReducer。
它的核心思想是:
把状态修改规则集中放到一个函数中。
8.2 useReducer 示例
javascript
import { useReducer } from 'react';
function reducer(state: number, action: string) {
switch (action) {
case 'add':
return state + 1;
case 'minus':
return state - 1;
case 'reset':
return 0;
default:
return state;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch('add')}>
+1
</button>
<button onClick={() => dispatch('minus')}>
-1
</button>
<button onClick={() => dispatch('reset')}>
重置
</button>
</div>
);
}
这里需要理解三个概念:
state:当前状态action:希望执行的操作dispatch:发送操作,触发状态计算
例如:
scss
dispatch('add');
可以理解为:
React,我要执行 add 操作,请根据 reducer 中的规则生成新的状态。
8.3 useState 和 useReducer 怎么选?
简单状态:
scss
const [visible, setVisible] = useState(false);
优先使用 useState。
多个相关状态、复杂状态转换:
scss
const [state, dispatch] = useReducer(reducer, initialState);
可以考虑 useReducer。
九、自定义 Hook:封装重复逻辑
9.1 什么叫自定义 Hook?
React 内置了 useState、useEffect 等 Hook。
但是实际开发中,经常会遇到重复的状态逻辑。
例如多个页面都需要:
- 请求任务列表
- 管理 loading
- 管理 error
- 保存接口数据
我们可以把这些逻辑封装为一个自定义 Hook。
9.2 示例:useTasks
scss
import { useEffect, useState } from 'react';
interface Task {
id: number;
name: string;
}
function useTasks() {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const controller = new AbortController();
async function loadTasks() {
try {
const response = await fetch('/api/tasks', {
signal: controller.signal
});
if (!response.ok) {
throw new Error('请求失败');
}
const data: Task[] = await response.json();
setTasks(data);
} catch (err) {
if (!controller.signal.aborted) {
setError('加载任务失败');
}
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
loadTasks();
return () => controller.abort();
}, []);
return { tasks, loading, error };
}
页面组件中直接使用:
javascript
function TaskPage() {
const { tasks, loading, error } = useTasks();
if (loading) {
return <p>加载中...</p>;
}
if (error) {
return <p>{error}</p>;
}
return (
<div>
{tasks.map(task => (
<p key={task.id}>{task.name}</p>
))}
</div>
);
}
这样,页面组件只需要关注页面如何展示,接口请求逻辑则放在 Hook 里。
注意:自定义 Hook 并不会自动让多个组件共享同一份状态。每次调用通常都有自己独立的状态。
十、Hook 的使用规则(必须掌握)
React Hook 并不是想在哪里调用就在哪里调用。
规则一:不要在条件判断中调用 Hook
错误写法:
scss
if (visible) {
const [count, setCount] = useState(0);
}
正确写法:
scss
const [count, setCount] = useState(0);
if (visible) {
// 使用 count
}
规则二:不要在循环中调用 Hook
错误写法:
ini
for (let i = 0; i < 10; i++) {
useState(0);
}
规则三:不要在普通事件函数中调用 Hook
错误写法:
scss
function handleClick() {
const [count, setCount] = useState(0);
}
React Hook 应该在 React 函数组件或自定义 Hook 的顶层调用。
为什么?
因为 React 需要依靠稳定的 Hook 调用顺序,将每次渲染中的状态正确对应起来。
可以记住:
Hook 放组件顶层,不能随意放进 if、for、事件函数中。
十一、实战:开发一个任务管理页面
现在把前面学到的内容组合起来。
需求:
- 页面显示任务列表。
- 输入任务名称。
- 点击按钮添加任务。
- 支持删除任务。
- 自动计算当前任务数量。
typescript
import { useState, useMemo } from 'react';
interface Task {
id: number;
name: string;
}
function TaskManager() {
// 1. 任务列表状态
const [tasks, setTasks] = useState<Task[]>([]);
// 2. 输入框状态
const [keyword, setKeyword] = useState('');
// 3. 添加任务
function addTask() {
const name = keyword.trim();
if (!name) return;
const newTask: Task = {
id: Date.now(),
name
};
setTasks(prev => [...prev, newTask]);
setKeyword('');
}
// 4. 删除任务
function deleteTask(id: number) {
setTasks(prev =>
prev.filter(task => task.id !== id)
);
}
// 5. 计算任务数量(这里其实不必用 useMemo)
const taskCount = tasks.length;
// 6. 渲染页面
return (
<div>
<h2>任务管理系统</h2>
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="请输入任务名称"
/>
<button onClick={addTask}>
添加任务
</button>
<p>当前任务数量:{taskCount}</p>
{tasks.map(task => (
<div key={task.id}>
<span>{task.name}</span>
<button onClick={() => deleteTask(task.id)}>
删除
</button>
</div>
))}
</div>
);
}
export default TaskManager;
11.1 执行流程
假设输入框输入:
数据回扫任务
执行:
scss
setKeyword('数据回扫任务');
触发组件重新渲染,输入框显示对应内容。
点击添加:
ini
setTasks(prev => [...prev, newTask]);
React 保存新的任务数组,并重新渲染任务列表。
这里:
ini
tasks.map(task => (
<div key={task.id}>
{task.name}
</div>
))
表示遍历任务数组,把每个任务渲染为一个元素。
key 用来帮助 React 识别列表中的元素。
点击删除:
ini
setTasks(prev =>
prev.filter(task => task.id !== id)
);
创建一个不包含指定任务的新数组,React 再次更新页面。
整个流程就是:
用户操作
↓
触发事件函数
↓
调用 setState
↓
React 重新渲染组件
↓
根据最新 State 生成页面
这也是 React 开发中最核心的思维方式。
十二、工作中应该优先掌握哪些 Hook?
| 优先级 | Hook | 掌握程度 |
|---|---|---|
| ⭐⭐⭐⭐⭐ | useState | 必须熟练 |
| ⭐⭐⭐⭐⭐ | useEffect | 必须熟练 |
| ⭐⭐⭐⭐ | useRef | 掌握常见场景 |
| ⭐⭐⭐⭐ | useContext | 能看懂、能使用 |
| ⭐⭐⭐ | useMemo | 理解缓存机制 |
| ⭐⭐⭐ | useCallback | 理解函数引用 |
| ⭐⭐⭐ | useReducer | 理解复杂状态管理 |
| ⭐⭐⭐⭐ | 自定义 Hook | 能理解和封装基础逻辑 |
React 还提供 useLayoutEffect、useId、useTransition、useDeferredValue 等其他 Hook,建议在掌握基础 Hook 后再学习。
最后总结
React Hooks 看起来很多,但可以按职责分类:
第一类:状态管理
useState:管理普通状态useReducer:管理复杂状态
第二类:副作用与引用
useEffect:与外部系统同步useRef:保存引用、操作 DOM
第三类:性能优化
useMemo:缓存计算结果useCallback:缓存函数引用
第四类:数据共享
useContext:读取跨组件共享的数据
第五类:逻辑复用
- 自定义 Hook:复用带有状态和其他 Hook 的逻辑
真正需要建立的是这个核心认知:
React 不是让我们直接修改 HTML 页面,而是让我们描述:当前数据状态下,页面应该长什么样。
用户操作改变状态,React 根据新状态更新页面。
理解这一点之后,Hook 的学习会容易很多。