React Hooks 详解:useState 与 useEffect 的使用与核心概念
在 React 的发展历程中,Hooks 的出现彻底改变了函数组件的能力。以use开头的 React Hooks,让函数组件能够轻松管理状态、处理副作用,实现了与类组件同等的功能,同时保持了代码的简洁与原生 JavaScript 风格。本文将结合实际代码,详细解析useState和useEffect这两个最核心的内置 Hooks。
一、React Hooks 概述
React Hooks 是 React 提供的一系列以use为前缀的函数 API,其核心作用是让函数组件能够使用状态(State)和其他 React 特性(如生命周期、上下文等)。与类组件相比,Hooks 使代码更简洁、逻辑更清晰,同时避免了类组件中this指向、生命周期嵌套等问题。
从本质上讲,Hooks 是对 React 特性的 “函数式封装”,让开发者可以用更接近原生 JavaScript 的方式编写 React 组件。本文重点讲解两个最基础的内置 Hooks:useState(状态管理)和useEffect(副作用处理)。
二、useState:管理组件的响应式状态
useState是 React 中用于管理组件状态的 Hook,它让函数组件拥有了 “记忆” 能力,能够保存并更新数据,实现页面的响应式渲染。
1. 基本用法与初始化
useState的基本语法如下:
const [state, setState] = useState(initialValue);
state:当前的状态值,初始值由initialValue决定setState:更新状态的函数,调用后会触发组件重新渲染initialValue:状态的初始值,可以是任意类型(基本类型、对象、数组等)
代码示例 :
import { useState } from 'react';
export default function App() {
// 用函数初始化复杂计算的初始值
const [num, setNum] = useState(() => {
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2; // 初始值为6
});
// ...
}
这里有一个重要细节:当初始值需要通过复杂计算得到时,推荐将initialValue写成一个纯函数(相同输入始终返回相同输出,无副作用)。这样的函数只会在组件初始化时执行一次,避免每次渲染都重复计算,提高性能。
2. 状态更新的两种方式
setState更新状态有两种方式,分别适用于不同场景:
(1)直接传入新值
当新状态不依赖于旧状态时,可以直接传入新值:
<div onClick={() => setNum(prevNum => prevNum + 1)}>{num}</div>
(2)传入更新函数(依赖旧状态时)
当新状态需要基于旧状态计算时,必须传入一个函数,该函数的参数为 “上一次的状态”(prevState):
<div onClick={() => setNum((prevNum) => {
console.log(prevNum + 1);
return prevNum + 1; // 基于旧值计算新值
})}>{num}</div>
这种方式的核心作用是避免状态更新的竞态问题。由于 React 的状态更新可能是异步的,直接使用num + 1可能获取到的是未更新的旧值,而通过prevState可以确保拿到的是最新的状态。
3. 注意事项
useState的初始值仅在组件第一次渲染时生效,后续渲染会忽略初始值- 初始值不能是异步操作的结果(如异步请求),因为状态必须是 “确定的”,而异步操作的结果无法保证同步获取
setState是 “替换” 而非 “合并” 状态(与类组件的this.setState不同),如果状态是对象,需要手动合并旧属性(如setUser(prev => ({ ...prev, name: 'new' })))
三、useEffect:处理组件的副作用
在 React 中,组件本身应该是 “纯函数”—— 即输入props后,输出固定的 JSX,不产生副作用。但实际开发中,我们需要处理数据请求、定时器、事件监听等 “副作用”(Side Effect),useEffect正是用于管理这些副作用的 Hook。
1. 基本语法与执行时机
useEffect的语法如下:
useEffect(() => {
// 副作用逻辑(如数据请求、定时器等)
return () => {
// 清理函数(可选):用于清除副作用(如清除定时器、取消订阅等)
};
}, [dependencies]); // 依赖项数组(可选)
useEffect的执行时机由第二个参数dependencies(依赖项数组)决定,主要分为三种情况:
(1)依赖项为空数组[]:仅在组件挂载时执行(模拟onMounted)
当依赖项为空数组时,useEffect只会在组件第一次渲染完成后执行一次,相当于 Vue 中的onMounted生命周期。
代码示例 :
useEffect(() => {
console.log('123123'); // 组件挂载时执行
const timer = setInterval(() => {
console.log('timer');
}, 1000);
// 清理函数:组件卸载时执行
return () => {
console.log('remove');
clearInterval(timer); // 清除定时器,避免内存泄漏
};
}, []); // 空依赖项:仅挂载时执行
这里的返回值是一个清理函数,它会在组件卸载前执行,用于清理副作用(如清除定时器、取消事件监听等),避免内存泄漏。
(2)依赖项为非空数组[state1, state2]:挂载时执行 + 依赖项变化时执行
当依赖项数组包含状态或变量时,useEffect会在组件挂载时执行一次,之后每当数组中的任意依赖项发生变化时,都会重新执行。
代码示例 :
// 当num变化时执行
useEffect(() => {
console.log(num, 'zzz'); // 挂载时执行一次,num更新时再次执行
}, [num]); // 依赖num:num变化则触发
这种方式常用于 “监听状态变化”,例如当某个状态更新后,执行对应的逻辑(如根据新的num重新请求数据)。
(3)无依赖项:每次渲染后都执行
如果不传入第二个参数,useEffect会在每次组件渲染完成后执行(包括初始挂载和每次更新)。
代码示例 :
// 无依赖项:每次渲染(挂载+更新)后都执行
useEffect(() => {
console.log('ddd');
})
这种方式需谨慎使用,频繁执行可能导致性能问题。
2. 清理函数的作用
useEffect的返回值(清理函数)有两个核心作用:
- 在下次执行副作用前清理上一次的副作用:例如当依赖项变化时,先清除旧的定时器,再创建新的定时器。
代码示例 :
useEffect(() => {
console.log('effect');
const timer = setInterval(() => {
console.log(num); // 依赖num的定时器
}, 1000);
return () => {
console.log('remove');
clearInterval(timer); // 下次执行effect前,先清除上一个定时器
};
}, [num]); // 依赖num:num变化时,先执行清理函数,再执行新的effect
当num变化时,React 会先调用清理函数清除旧定时器,再执行新的副作用创建新定时器,避免多个定时器同时运行。
- 组件卸载时清理副作用:例如组件被销毁时,清除定时器、取消网络请求等,防止内存泄漏。
3. 常见副作用场景
useEffect适用于所有 “非纯函数” 操作,常见场景包括:
- 数据请求(如 App.jsx 中的
queryData) - 定时器 / 间隔器(
setTimeout/setInterval) - 事件监听(
addEventListener) - DOM 操作(如手动修改 DOM 样式)
四、核心概念总结
-
纯函数与副作用:
- 纯函数:输入固定时输出固定,无外部影响(如组件根据
props和state输出 JSX) - 副作用:影响组件外部环境或需要清理的操作(如数据请求、定时器等),由
useEffect管理
- 纯函数:输入固定时输出固定,无外部影响(如组件根据
-
依赖项数组:
- 决定
useEffect的执行时机,是性能优化的关键 - 必须包含所有在副作用中使用的状态 / 变量,否则可能因 “闭包陷阱” 获取到旧值
- 决定
-
闭包特性:
useEffect的副作用函数和清理函数会形成闭包,捕获当前渲染周期的状态值- 当依赖项更新时,会创建新的闭包,从而获取最新的状态
五、总结
useState让函数组件拥有了响应式状态,支持基于旧状态的更新,初始化时可通过纯函数处理复杂计算useEffect统一管理副作用,通过依赖项数组控制执行时机,清理函数避免内存泄漏