React 16.8 推出了革命性的 Hooks 特性,让函数组件拥有了管理状态和处理副作用的能力。相比于传统的类组件,Hooks 提供了更简洁、更灵活的函数式编程体验。本文将重点介绍两个核心 Hooks ------ useState
和 useEffect
,并结合组件生命周期,详细讲解如何高效管理状态和副作用。
一、useState ------ 函数组件的状态管理利器
useState
是 React 用来在函数组件中声明状态的 Hook。它让函数组件拥有了类似类组件中 this.state
的功能,但写法更简洁。
基本用法
javascript
import React, { useState } from 'react';
function Counter() {
// 声明一个叫 count 的状态变量,初始值为 0
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>点击增加</button>
</div>
);
}
export default Counter;
说明
useState
返回一个数组,第一个元素是当前状态,第二个是更新状态的函数。- 每次调用
setCount
,组件会重新渲染,显示最新状态。
二、useEffect ------ 副作用处理的"守护神"
在 React 中,副作用指的是除了渲染以外需要执行的操作,比如数据请求、DOM 操作、事件绑定等。
useEffect
用于在函数组件中处理这些副作用,等效于类组件中的生命周期函数。
1. useEffect 是什么?
useEffect
是 React Hooks 中专门用来处理"副作用"的函数。副作用(side effect)是指那些影响到组件之外的操作,例如:
- 网络请求(fetch 数据)
- 订阅事件(如监听滚动、WebSocket)
- 操作 DOM(手动聚焦输入框)
- 设置定时器(
setTimeout
、setInterval
) - 日志记录等
React 渲染本身是"纯粹的",但现实中我们不可避免需要执行这些副作用,而 useEffect
正是帮我们优雅管理它们的工具。
2. useEffect 的基本语法
scss
useEffect(() => {
// 副作用代码
return () => {
// 清理副作用代码(可选)
};
}, [依赖项]);
- 第一个参数是一个副作用函数(effect function),里面写需要执行的副作用代码。
- 第二个参数是"依赖项数组",用来告诉 React 什么时候重新执行这个副作用。
3. useEffect 的执行时机
- 组件挂载后
useEffect
会在组件第一次渲染到页面之后执行。它的执行时机是在浏览器绘制完成之后,因此不会阻塞页面渲染。 - 依赖项变化后
如果你传入了依赖项数组,那么只有当数组中的某个依赖发生变化时,useEffect
中的副作用才会重新执行。 - 组件卸载前
useEffect
中可以返回一个"清理函数",React 会在组件卸载时调用这个清理函数,以便释放资源、防止内存泄漏。
4. 依赖项数组详解
-
不传依赖项数组
javascriptuseEffect(() => { console.log('每次渲染后都会执行'); });
每次组件渲染(包括首次和更新)后,都会执行副作用函数。
-
传空数组
[]
scssuseEffect(() => { console.log('只在组件挂载时执行一次'); }, []);
只会在组件挂载时执行一次,类似于类组件的
componentDidMount
。 -
传有依赖的数组
scssuseEffect(() => { console.log('依赖 count,count 变化时执行'); }, [count]);
仅在依赖发生变化时执行副作用。
5. 清理函数(Cleanup)
useEffect
允许返回一个函数,这个函数会在以下时机被调用:
- 组件卸载前(类似
componentWillUnmount
) - 下次副作用执行前(如果依赖项发生变化)
清理函数常用来:
- 清除定时器
- 取消事件监听
- 取消网络请求(配合 AbortController)
- 取消订阅
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log('计时器执行');
}, 1000);
return () => {
clearInterval(timer); // 清理定时器,避免内存泄漏
};
}, []);
6. useEffect 不能直接写 async 的原因
useEffect
的回调函数不允许是异步函数,因为异步函数会返回 Promise,而 React 期望副作用函数返回清理函数或 undefined
。
错误示例:
dart
useEffect(async () => {
const res = await fetch('/api');
// 错误:useEffect 不接受 async 函数
});
正确写法:
scss
useEffect(() => {
async function fetchData() {
const res = await fetch('/api');
// 处理数据
}
fetchData();
}, []);
7. 常见问题与注意事项
-
依赖项遗漏问题
如果依赖项数组中没有包含所有用到的外部变量,可能导致副作用函数中使用的变量不是最新值,容易出现 bug。
React 官方 ESLint 插件会提醒你补全依赖。
-
避免无限循环
如果副作用中更新了状态,且状态是依赖项,会导致副作用无限执行。需要合理设计依赖项,或通过条件判断避免。
-
网络请求的取消
组件卸载时未取消请求,可能导致内存泄漏和更新已卸载组件的错误。配合
AbortController
实现取消:iniuseEffect(() => { const controller = new AbortController(); fetch('/api/data', { signal: controller.signal }) .then(res => res.json()) .then(data => { // 更新状态 }) .catch(err => { if (err.name === 'AbortError') { console.log('请求被取消'); } }); return () => { controller.abort(); // 取消请求 }; }, []);
三、组件生命周期与 Hooks 对应关系
生命周期阶段 | 类组件函数 | Hooks 方式 |
---|---|---|
挂载后 (mounted) | componentDidMount | useEffect(() => {}, []) |
更新后 (updated) | componentDidUpdate | useEffect(() => {}, [依赖]) |
卸载时 (unmounted) | componentWillUnmount | useEffect 返回清理函数 |
四、组件请求接口的最佳实践
- 什么时候请求?
组件挂载后发起请求,确保界面快速展示数据。 - 怎么请求?
将请求逻辑放入useEffect
中,避免阻塞渲染。 - 避免多次请求
设置依赖项为空数组[]
,保证请求只执行一次。
javascript
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
async function fetchUser() {
const res = await fetch(`https://api.example.com/users/${userId}`);
const data = await res.json();
setUser(data);
}
fetchUser();
return () => {
// 这里可以做一些清理操作,比如取消请求等
console.log('组件卸载或 userId 变更,进行清理');
};
}, [userId]);
if (!user) return <div>加载中...</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
export default UserProfile;
五、总结
useState
简化了状态管理,赋予函数组件状态能力。useEffect
专注于副作用,涵盖挂载、更新和卸载时机。- 请求接口时,建议放在
useEffect
,且依赖项为空数组,确保只请求一次。 useEffect
中不能直接写async
,需在内部声明异步函数。- 一定要合理清理副作用,避免内存泄漏。
- 理解依赖数组机制,避免遗漏或无限循环。
React Hooks 让函数组件更加强大和灵活,理解并掌握 useState
和 useEffect
,将显著提升你的 React 开发效率和代码质量。