从零开始学 React Hooks:useState 与 useEffect 核心解析
作为 React 官方主推的语法,Hooks 让函数组件拥有了状态管理和生命周期的能力,彻底摆脱了类组件的繁琐语法,让 React 代码更贴近原生 JS。本文从纯函数与副作用的基础概念出发,由浅入深讲解useState和useEffect两个核心 Hooks 的使用,适合 JS 初学者快速上手,所有案例均基于实战代码拆解,易懂易练。
一、前置基础:纯函数与副作用
在学习 Hooks 前,必须先理解纯函数和副作用这两个核心概念,它们是 Hooks 设计的底层逻辑,也是 React 组件设计的重要原则。
1.1 纯函数
纯函数是相同输入始终返回相同输出,且无任何副作用的同步函数,这是纯函数的三大核心特征:
- 输入确定,输出确定:不会因外部变量、环境变化改变返回结果
- 无副作用:不修改函数外部的变量、不操作 DOM、不发起网络请求等
- 必须同步:不包含异步操作(异步会导致返回结果不确定)
纯函数示例:
// 纯函数:输入x和y,输出固定的和,无任何外部影响
const add = (x, y) => x + y;
// React中useState的初始值计算函数也是纯函数
const getInitNum = () => {
const a = 1 + 2;
const b = 2 + 3;
return a + b; // 输入固定,返回值永远是8
};
1.2 副作用
副作用是指函数执行过程中,对函数外部环境产生的一切影响,简单来说:非纯函数的操作,基本都是副作用。
常见的副作用场景:
- 修改函数外部的变量、数组、对象(如给数组 push 元素)
- 发起网络请求(
fetch/axios)、定时器 / 延时器(setTimeout/setInterval) - 操作 DOM、本地存储(
localStorage) - 订阅 / 取消订阅事件
副作用示例:
// 有副作用:修改了外部的nums2数组
function add(nums2) {
nums2.push(3); // 改变外部变量,副作用
return nums2.reduce((pre, cur) => pre + cur, 0);
}
const nums2 = [1, 2];
add(nums2);
console.log(nums2); // [1,2,3],原数组被修改
// 有副作用:包含网络请求(不确定操作)
const add2 = (x, y) => {
fetch('https://www.baidu.com'); // 网络请求,副作用
return x + y;
};
1.3 组件与纯函数的关系
React 函数组件的核心逻辑应该是纯函数:输入 props/state,输出固定的 JSX,不包含副作用。而所有的副作用操作,都需要交给专门的 Hooks 来处理(如useEffect),这是 React 的设计规范,能保证组件的可预测性和稳定性。
二、useState:让函数组件拥有响应式状态
useState是 React 最基础的 Hooks,作用是为函数组件添加响应式状态,并提供修改状态的方法。状态(state)就是组件中会变化的数据,也是组件的核心,状态变化时,组件会自动重新渲染,更新页面内容。
2.1 基本使用
语法:
import { useState } from 'react';
// 解构赋值:state为当前状态值,setState为修改状态的方法
const [state, setState] = useState(initialValue);
initialValue:状态的初始值,可以是任意 JS 类型(数字、字符串、数组、对象等)state:获取当前的状态值setState:修改状态的方法,调用后会更新 state 并触发组件重新渲染
基础示例:
import { useState } from 'react'
export default function App(){
// 初始化数字状态,初始值为1
const [num, setNum] = useState(1);
return (
// 点击div,修改num状态
<div onClick={() => setNum(num + 1)}>
当前数字:{num}
</div>
)
}
点击页面中的 div,数字会逐次加 1,页面自动更新,这就是响应式状态的核心效果。
2.2 高级用法 1:函数式初始化
如果状态的初始值需要复杂计算(如多个变量运算、循环处理),直接传值会导致每次组件渲染都重复计算,造成性能浪费。此时可以使用函数式初始化,该函数只会在组件首次挂载时执行一次,后续渲染不再执行。
语法:
// 传入纯函数,返回值作为初始值
const [state, setState] = useState(() => {
// 复杂的同步计算逻辑(纯函数,无异步、无副作用)
return 计算后的初始值;
});
实战示例:
import { useState } from 'react'
export default function App(){
// 函数式初始化:仅首次挂载执行,计算初始值为8
const [num, setNum] = useState(() => {
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2;
});
return (
<div onClick={() => setNum(num + 1)}>
初始值计算后:{num}
</div>
)
}
⚠️ 注意:初始化的函数必须是纯函数,不能包含异步操作(如setTimeout、网络请求),因为异步会导致初始值不确定,而 React 要求状态的初始值必须是确定的。
2.3 高级用法 2:函数式更新状态
修改状态时,setState不仅可以直接传入新值,还可以传入一个函数,该函数的参数是上一次的状态值,返回值为新的状态值。
适用场景:当新的状态值依赖于上一次的状态值时,推荐使用函数式更新,能避免因 React 状态更新的异步性导致的取值错误。
语法:
setState(preState => {
// preState:上一次的状态值(React自动传入)
return 新的状态值;
});
实战示例:
import { useState } from 'react'
export default function App(){
const [num, setNum] = useState(1);
return (
// 函数式更新:preNum为上一次的num值
<div onClick={() => setNum((preNum) => {
console.log('上一次的数字:', preNum);
return preNum + 1; // 返回新值
})}>
当前数字:{num}
</div>
)
}
点击 div 时,会先打印上一次的数字,再返回新值,确保状态更新的准确性。
2.4 核心注意点
useState必须在函数组件的顶层调用,不能在 if、for、嵌套函数中使用(React 通过调用顺序识别 Hooks)setState是异步操作,调用后不能立即获取到新的状态值- 状态更新是不可变的:如果状态是对象 / 数组,不能直接修改原数据,需返回新的对象 / 数组(如
setArr(pre => [...pre, newItem]))
三、useEffect:处理组件的所有副作用
useEffect是 React 处理副作用的核心 Hooks,作用是在函数组件中执行副作用操作,同时它还能模拟类组件的生命周期(如挂载、更新、卸载),让函数组件拥有了生命周期的能力。
3.1 基本概念
useEffect的直译是副作用效果,专门用来包裹组件中的所有副作用代码- 组件的核心逻辑(纯函数)负责渲染 JSX,副作用逻辑(请求、定时器、DOM 操作)全部放在
useEffect中 useEffect接收两个参数:副作用函数和依赖项数组
3.2 基本语法
import { useEffect } from 'react';
useEffect(() => {
// 副作用函数:执行所有副作用操作(请求、定时器、DOM操作等)
// 可选:返回一个清理函数
return () => {
// 清理函数:清除副作用(如清除定时器、取消订阅、关闭请求)
};
}, [deps]); // 依赖项数组:控制useEffect的执行时机
3.3 三种使用场景(核心)
useEffect的执行时机完全由第二个参数(依赖项数组) 控制,分为三种核心场景,对应组件的不同生命周期阶段,这是useEffect的重点,一定要掌握!
场景 1:无依赖项数组 → 每次渲染都执行
useEffect(() => {
console.log('每次渲染/更新都会执行');
});
- 组件首次挂载时执行一次
- 组件每次状态更新 / 重新渲染时都会再次执行
- 适用场景:需要实时响应组件所有变化的副作用(较少使用,注意性能)
场景 2:空依赖项数组 [] → 仅组件挂载时执行一次
useEffect(() => {
console.log('仅挂载时执行,模拟onMounted');
// 示例:挂载时发起异步请求
queryData().then(data => setNum(data));
}, []);
- 仅在组件首次挂载到 DOM时执行一次,后续无论状态如何更新,都不会再执行
- 对应类组件的
componentDidMount生命周期,是最常用的场景 - 适用场景:初始化请求数据、初始化定时器、添加全局事件监听等
场景 3:有依赖项的数组 [state1, state2] → 依赖项变化时执行
const [num, setNum] = useState(0);
useEffect(() => {
console.log('num变化时执行', num);
}, [num]); // 依赖项为num
- 组件首次挂载时执行一次
- 只有当依赖项数组中的值发生变化时,才会再次执行
- 对应类组件的
componentDidUpdate生命周期 - 适用场景:依赖某个 / 某些状态的副作用(如状态变化时更新定时器、重新请求数据)
3.4 清理函数:清除副作用(避免内存泄漏)
useEffect的副作用函数可以返回一个清理函数,这是 React 的重要设计,用于清除副作用,避免内存泄漏。
清理函数的执行时机
- 当组件重新渲染,且
useEffect即将再次执行时,先执行上一次的清理函数 - 当组件从 DOM 中卸载时,执行清理函数
核心使用场景:清除定时器 / 延时器
定时器是最常见的副作用,如果不及时清除,组件卸载后定时器仍会运行,导致内存泄漏,useEffect的清理函数完美解决这个问题。
实战示例:
import { useState, useEffect } from 'react'
export default function App() {
const [num, setNum] = useState(0);
useEffect(() => {
console.log('num更新,创建新定时器');
// 创建定时器:每秒打印当前num
const timer = setInterval(() => {
console.log(num);
}, 1000);
// 返回清理函数:清除上一次的定时器
return () => {
console.log('清除定时器');
clearInterval(timer);
};
}, [num]); // 依赖num,num变化时执行
return (
<div onClick={() => setNum(pre => pre + 1)}>
点击修改num:{num}
</div>
)
}
执行效果:
- 组件挂载时,创建定时器,每秒打印 num
- 点击 div 修改 num,
useEffect先执行清理函数清除旧定时器,再创建新定时器 - 组件卸载时,执行清理函数清除定时器,避免内存泄漏
其他清理场景
- 取消网络请求(如 AbortController)
- 移除全局事件监听(如
window.removeEventListener) - 取消订阅(如 Redux 订阅、WebSocket 订阅)
3.5 实战:结合 useEffect 实现异步请求初始化数据
前面提到,useState的函数式初始化不支持异步,因此组件挂载时的异步请求数据,需要结合useEffect(空依赖)实现,这是项目中的高频用法。
实战示例:
import { useState, useEffect } from 'react'
// 模拟异步请求接口
async function queryData() {
const data = await new Promise(resolve => {
setTimeout(() => {
resolve(666); // 模拟接口返回数据
}, 2000);
});
return data;
}
export default function App() {
const [num, setNum] = useState(0);
// 空依赖:仅挂载时请求数据
useEffect(() => {
queryData().then(data => {
setNum(data); // 请求成功后修改状态,更新页面
});
}, []);
return <div>接口返回数据:{num}</div>;
}
组件挂载后,发起异步请求,请求成功后修改num状态,页面自动更新为接口返回的 666。
四、Hooks 的通用使用规则
除了useState和useEffect,React 所有的 Hooks(包括自定义 Hooks)都遵循以下两条核心规则,这是 React 官方强制要求的,违反会导致组件运行异常:
4.1 只能在函数组件 / 自定义 Hooks 中调用
Hooks 只能在React 函数组件的顶层,或者自定义 Hooks中调用,不能在普通 JS 函数、类组件中使用。
4.2 只能在顶层调用,不能嵌套
Hooks 不能在 if、for、while、嵌套函数(如 useEffect 的副作用函数)中调用,必须在函数组件的顶层作用域调用。因为 React 通过调用顺序来识别和管理每个 Hooks 的状态,如果嵌套调用,会导致调用顺序混乱,Hooks 状态失效。
五、实战综合案例:条件渲染 + 副作用清理
结合useState的状态管理、useEffect的副作用处理、React 的条件渲染,实现一个完整的小案例,覆盖本文所有核心知识点:
- 点击页面修改数字状态,数字为偶数时渲染
Demo组件,奇数时卸载 Demo组件挂载时创建定时器,卸载时清除定时器- 主组件的数字变化时,更新定时器并实时打印
主组件 App.jsx:
import { useState, useEffect } from 'react'
import Demo from './Demo';
export default function App() {
const [num, setNum] = useState(0);
// 依赖num的副作用,处理定时器
useEffect(() => {
const timer = setInterval(() => {
console.log('当前num:', num);
}, 1000);
return () => clearInterval(timer);
}, [num]);
// 条件渲染:num为偶数时渲染Demo组件
return (
<div onClick={() => setNum(pre => pre + 1)} style={{ fontSize: '24px' }}>
点击修改数字:{num}
{num % 2 === 0 && <Demo />}
</div>
)
}
子组件 Demo.jsx:
import { useEffect } from 'react'
export default function Demo() {
// 空依赖:仅挂载时创建定时器,卸载时清除
useEffect(()=>{
console.log('Demo组件挂载');
const timer=setInterval(()=>{
console.log('Demo组件的定时器');
},1000)
// 组件卸载时执行,清除定时器
return ()=>{
console.log('Demo组件卸载,清除定时器');
clearInterval(timer)
}
},[])
return <div style={{ marginTop: '20px' }}>我是偶数时显示的Demo组件</div>
}
案例效果:
- 初始 num=0(偶数),渲染 Demo 组件,Demo 挂载并创建定时器
- 点击一次 num=1(奇数),卸载 Demo 组件,执行 Demo 的清理函数清除定时器
- 每次点击修改 num,主组件的
useEffect都会先清除旧定时器,再创建新定时器 - 组件卸载时,所有定时器都会被清除,无内存泄漏
六、总结
本文从基础的纯函数与副作用出发,讲解了 React 中最核心的两个 Hooks,核心知识点总结如下:
- 纯函数:相同输入返回相同输出,无副作用、同步执行;副作用:修改外部变量、请求、定时器等对外部环境的操作
- useState:为函数组件添加响应式状态,支持函数式初始化(复杂计算)和函数式更新(依赖上一次状态)
- useEffect:处理所有副作用,通过依赖项数组控制执行时机,返回清理函数清除副作用,避免内存泄漏
- Hooks 通用规则:仅在函数组件 / 自定义 Hooks 的顶层调用
- 异步请求初始化数据:使用
useEffect空依赖实现,而非useState的初始化函数
useState和useEffect是 React Hooks 的基础,掌握这两个 Hooks,就能实现大部分函数组件的开发需求。后续可以继续学习useRef、useContext、useReducer等进阶 Hooks,以及自定义 Hooks 的封装,让 React 代码更简洁、更高效。
最后:建议大家跟着本文的案例手动敲一遍代码,体会状态更新和副作用执行的时机,只有实战才能真正掌握 Hooks 的核心逻辑!