React Hooks 完全指南:useState 与 useEffect 核心用法
React Hooks 完全指南:useState 与 useEffect 核心用法
一、Introduction
React Hooks 是 React 16.8 引入的核心特性,它让你在函数组件中使用 state 和生命周期方法,彻底改变了 React 的开发范式。在此之前,只有 class 组件才能使用 state 和生命周期函数,而 Hooks 的出现让函数组件拥有了完整的能力,使得代码更简洁、更易于复用和测试。
本文重点讲解 useState(状态管理)和 useEffect(副作用处理)这两个最基础也最常用的 Hook,涵盖它们的完整用法、常见陷阱以及实际开发中的最佳实践。
---
二、useState 基础语法
#### 2.1 基本用法
import React, { useState } from 'react';
function Counter() {
// 声明一个名为 count 的 state 变量,初始值为 0
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
{/* 点击按钮调用 setCount 更新状态,组件自动重新渲染 */}
<button onClick={() => setCount(count + 1)}>加 1</button>
<button onClick={() => setCount(count - 1)}>减 1</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
);
}
export default Counter;
#### 2.2 useState 的独家特性:函数式更新
当新状态依赖于旧状态时,推荐使用函数式更新,避免闭包陷阱:
function CounterFunctional() {
const [count, setCount] = useState(0);
// 错误写法:依赖外部变量,在快速点击时可能漏掉更新
const handleIncrementWrong = () => {
setCount(count + 1); // count 是闭包中的旧值
setCount(count + 1); // 两次调用用的是同一个旧值
// 结果:只增加了 1,而不是 2
};
// 正确写法:使用函数式更新,基于最新状态计算
const handleIncrementCorrect = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
// 结果:正确增加 2
};
return (
<div>
<p>计数:{count}</p>
<button onClick={handleIncrementWrong}>错误方式(只+1)</button>
<button onClick={handleIncrementCorrect}>正确方式(+2)</button>
</div>
);
}
#### 2.3 使用对象或数组作为状态
function UserProfile() {
// 状态可以是任何类型:对象
const [user, setUser] = useState({
name: '张三',
age: 28,
email: 'zhangsan@example.com'
});
// 更新对象状态时,需要展开旧对象,再覆盖要修改的字段
const updateAge = (newAge) => {
setUser(prevUser => ({
...prevUser,
age: newAge
}));
};
// 注意:不能直接修改状态,必须通过 setState 创建新对象
const updateName = (newName) => {
setUser({ ...user, name: newName }); // 先展开,再用新值覆盖
};
return (
<div>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
<p>邮箱:{user.email}</p>
<button onClick={() => updateAge(user.age + 1)}>生日+1岁</button>
<button onClick={() => updateName('李四')}>改名为李四</button>
</div>
);
}
---
三、useEffect 基础语法
#### 3.1 什么是副作用(Side Effect)
副作用是指那些影响组件外部世界的操作,比如:发送 HTTP 请求、操作 DOM、订阅事件、设置定时器等。useEffect 就是用来在组件中处理这些副作用的。
#### 3.2 基本用法
import React, { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// useEffect 接收两个参数:
// 第一个是副作用处理函数(回调)
// 第二个是依赖数组(可选),控制何时重新执行
useEffect(() => {
// 这个函数在组件挂载后执行一次(类似 componentDidMount)
console.log('组件已挂载,开始获取数据');
// 使用 AbortController 在组件卸载时取消请求,避免内存泄漏
const controller = new AbortController();
fetch('https://jsonplaceholder.typicode.com/posts/1', {
signal: controller.signal
})
.then(response => {
if (!response.ok) throw new Error('网络错误');
return response.json();
})
.then(jsonData => {
setData(jsonData);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
}
});
// 清理函数:组件卸载或依赖变化时执行(类似 componentWillUnmount)
return () => {
console.log('清理:取消未完成的请求');
controller.abort();
};
}, []); // 空依赖数组:只在挂载时执行一次
if (loading) return <div>加载中...</div>;
if (error) return <div>错误:{error}</div>;
return (
<div>
<h2>{data.title}</h2>
<p>{data.body}</p>
</div>
);
}
---
四、运行效果说明
以上 Counter 组件渲染后:
- 页面显示"当前计数:0"和三个按钮
- 点击"加 1",count 从 0 变为 1,组件重新渲染,数字更新为 1
- 快速连续点击时,函数式更新确保每次都基于最新值计算
- 对象/数组状态更新时,展开运算符确保不可变性,其他字段保留
DataFetcher 组件:
- 挂载时自动发起 HTTP 请求
- loading 状态显示"加载中..."
- 请求成功后显示文章标题和正文
- 组件卸载时自动取消 pending 请求,防止 React 警告
---
五、常见问题(FAQ)
#### Q1:useState 的初始值会不会每次渲染都执行?
不会。 useState 的初始值只用在第一次渲染时(类似构造函数),后续渲染即使传入新的初始值,也会被忽略,除非你显式调用 setState 触发更新。
// 这个 console.log 只在首次渲染时打印一次
const [count, setCount] = useState(() => {
console.log('计算初始值'); // 只执行一次
return 0;
});
如果你有昂贵的计算逻辑,可以传入一个函数(lazy initial state),这样只在首次渲染时执行一次计算。
#### Q2:useEffect 的依赖数组应该怎么填?
原则:所有在 effect 中使用的变量,都应加入依赖数组。
// 错误:userId 变了但 effect 没有重新执行
useEffect(() => {
fetchUser(userId); // userId 是外部变量
}, []); // 缺少 userId 依赖
// 正确:把 userId 加入依赖数组
useEffect(() => {
fetchUser(userId);
}, [userId]); // userId 变化时重新执行
如果确实不需要重新执行,传空数组 []。但要确保 effect 内部的代码不会依赖任何会变化的变量。
#### Q3:为什么应该在 useEffect 中返回一个清理函数?
清理函数防止两类问题:内存泄漏和竞态条件。
useEffect(() => {
const timer = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// 如果没有这个清理函数,组件卸载后 timer 仍在运行
// 导致:Warning: Can't perform a React state update on an unmounted component
return () => clearInterval(timer);
}, []);
---
六、延伸阅读
- [React useRef 完全指南:操作 DOM 与持久化变量]()
- [React useCallback 与 useMemo 性能优化指南]()
- [React 自定义 Hooks 实战:逻辑复用与模块化]()
- [React Context API 状态管理深度理解]()