一、先用一句话概括这三个 Hook
如果你现在还很懵,先别慌,先记住下面这三句话。
useState
让组件记住会影响页面展示的数据
useEffect
让组件在渲染后去执行额外操作
useRef
让组件保存一个不会触发重新渲染的值,或者拿到 DOM 元素
这三句话,已经把它们最本质的区别说出来了。
如果还觉得抽象,没关系,接下来我一个个拆开讲。
二、先说 useState:它是"状态管理"的
React 组件最大的特点之一,就是:
数据一变,页面跟着变。
而 useState,就是专门用来保存这种"会驱动页面变化的数据"的。
先看最经典的例子。
javascript
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
当前点击了 {count} 次
</button>
);
}
export default Counter;
这里这句最关键:
scss
const [count, setCount] = useState(0);
它的意思你可以直接翻译成人话:
React,帮我准备一个状态,初始值是 0,当前值叫 count,修改它的方法叫 setCount。
也就是说:
count是当前状态值setCount是更新状态的方法0是初始值
当你点击按钮执行:
scss
setCount(count + 1);
React 会做两件事:
- 更新状态值
- 重新渲染组件
所以页面上的 count 就会变。
useState 最典型的应用场景
useState 常用于这些地方:
- 计数器数字
- 输入框内容
- 弹窗是否显示
- 下拉框选中项
- 当前分页页码
- 列表数据
- 加载状态
loading - 错误提示信息
比如控制弹窗:
scss
const [visible, setVisible] = useState(false);
比如保存输入框内容:
scss
const [keyword, setKeyword] = useState("");
比如保存接口返回的数据:
ini
const [list, setList] = useState([]);
这些都属于:
一旦数据变化,页面就要跟着变化。
这时候就应该用 useState。
三、再说 useEffect:它是"副作用处理"的
很多人第一次看到"副作用"这个词,容易被吓到。
其实它没有那么玄乎。
你可以简单把副作用理解成:
除了渲染页面以外,还要额外做的事情。
比如:
- 请求接口
- 设置定时器
- 监听事件
- 修改浏览器标题
- 操作本地存储
- 手动操作 DOM
- 组件销毁时做清理
这些都不是"渲染 JSX"本身,而是页面渲染之后要顺便做的事。
这时候就轮到 useEffect 出场了。
先看一个最简单的例子:
javascript
import React, { useEffect } from "react";
function Demo() {
useEffect(() => {
console.log("组件渲染完成了");
}, []);
return <div>Hello React</div>;
}
这段代码的意思就是:
页面渲染完以后,执行
console.log
所以你可以理解成:
useEffect = 渲染后执行任务
useEffect 最常见的使用场景
1. 请求接口
scss
useEffect(() => {
fetch("/api/user")
.then((res) => res.json())
.then((data) => {
console.log(data);
});
}, []);
2. 设置定时器
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log("每秒执行一次");
}, 1000);
return () => clearInterval(timer);
}, []);
3. 监听事件
javascript
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
4. 修改页面标题
ini
useEffect(() => {
document.title = "用户中心";
}, []);
这些都是副作用。
也就是说:
只要不是单纯为了渲染页面,而是渲染后还要做点别的事,大概率就要想到 useEffect。
四、再说 useRef:它是"持久容器"和"DOM 引用"
useRef 是很多初学者最容易迷糊的 Hook。
因为它不像 useState 那么直观,也不像 useEffect 那么容易理解成"执行动作"。
其实 useRef 可以简单理解成两个作用。
作用一:获取 DOM 元素
比如你想让输入框在页面加载后自动获取焦点:
javascript
import React, { useEffect, useRef } from "react";
function InputFocus() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="请输入内容" />;
}
export default InputFocus;
这里可以这样理解:
useRef(null)创建一个引用对象inputRef.current会指向真实的 input DOM- 通过
focus()就可以让输入框聚焦
也就是说:
useRef 可以帮你"拿到页面中的真实元素"。
作用二:保存一个值,但不触发页面重新渲染
这是 useRef 更重要、也更容易被忽略的能力。
比如保存定时器 id:
ini
const timerRef = useRef(null);
赋值:
ini
timerRef.current = setInterval(() => {
console.log("running");
}, 1000);
清除:
scss
clearInterval(timerRef.current);
这个值会一直保留在组件生命周期里,但它变化时不会导致页面重渲染。
所以你可以把 useRef 理解成:
组件里的一个"小盒子",你可以往里面放东西,它会一直记着,但不会因为盒子里的东西变了就刷新页面。
五、它们三个最大的区别,到底是什么?
这是本文最核心的部分。
我先直接给你一个最重要的结论:
| Hook | 核心作用 | 数据变化后会不会触发重新渲染 |
|---|---|---|
useState |
保存状态 | 会 |
useEffect |
执行副作用 | 本身不是存数据的 |
useRef |
保存引用/持久值 | 不会 |
把这张表吃透,你就不容易乱用了。
接下来我一个个解释。
六、useState 和 useRef 的区别,初学者最容易搞混
很多人学到这里时,最大的疑问就是:
既然 useState 能存值,useRef 也能存值,那到底啥时候用谁?
答案非常简单:
需要更新页面的,用 useState
不需要更新页面的,用 useRef
来看例子。
场景 1:页面上要显示这个值
javascript
import React, { useState } from "react";
function Demo() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前数字:{count}</p>
<button onClick={() => setCount(count + 1)}>加一</button>
</div>
);
}
这里 count 是显示在页面上的。
点击按钮后,页面中的数字也要变化。
所以必须用 useState。
场景 2:只是内部记一下,不需要显示
javascript
import React, { useRef } from "react";
function Demo() {
const clickTimesRef = useRef(0);
const handleClick = () => {
clickTimesRef.current += 1;
console.log("点击次数:", clickTimesRef.current);
};
return <button onClick={handleClick}>点击我</button>;
}
这里点击次数只是打印在控制台,并没有显示在页面上。
那就没必要用 useState,用 useRef 就够了。
再总结一遍
用 useState 的场景
- 页面要展示这个数据
- 数据变化后希望组件重新渲染
- 数据会驱动 UI 更新
用 useRef 的场景
- 只是临时保存一个值
- 不希望因为这个值变化而重新渲染
- 保存 DOM、定时器 id、上一次值等
七、为什么 useRef 改了值,页面不更新?
这个问题特别经典,面试也爱问。
比如下面这段代码:
javascript
import React, { useRef } from "react";
function Demo() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log(countRef.current);
};
return (
<div>
<p>{countRef.current}</p>
<button onClick={handleClick}>点击</button>
</div>
);
}
很多初学者会以为点击按钮后,页面上的数字会变。
但实际上,页面大概率不会更新。
为什么?
因为:
修改 ref.current 不会触发组件重新渲染。
React 只会在这些情况下重新渲染组件:
props变了state变了- 父组件重新渲染导致子组件重新渲染
而 ref.current 的变化,不在 React 的"响应式更新系统"里。
所以它改了,React 不会主动刷新页面。
这就是 useRef 和 useState 最大的区别之一。
八、useEffect 和 useState 的关系是什么?
开发中经常看到这俩一起出现。
比如页面加载后请求数据:
javascript
import React, { useEffect, useState } from "react";
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
});
}, []);
return (
<ul>
{users.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
这里的配合方式非常典型:
useState负责存数据useEffect负责获取数据
也就是说:
useState管"保存结果",useEffect管"执行动作"。
你可以理解成:
useState是仓库useEffect是工人- 工人出去搬货,最后把货放进仓库里
这是它们最经典的协作模式。
九、useEffect 的依赖数组到底怎么理解?
这个问题,是 React 初学者最容易卡壳的地方之一。
我们先看写法:
scss
useEffect(() => {
console.log("执行副作用");
}, []);
第二个参数 [],就叫 依赖数组。
它决定这个副作用什么时候执行。
1. 传空数组 []
scss
useEffect(() => {
console.log("只执行一次");
}, []);
表示:
组件首次渲染完成后执行一次。
常见用途:
- 页面加载请求一次接口
- 初始化某些逻辑
- 绑定事件监听并在销毁时清理
2. 不传依赖数组
javascript
useEffect(() => {
console.log("每次渲染都执行");
});
表示:
组件每次渲染后都会执行。
这个一般要慎用,否则可能造成不必要的执行。
3. 传某个依赖项
scss
useEffect(() => {
console.log("count 变化了");
}, [count]);
表示:
首次渲染执行一次,以后只有 count 变化时才执行。
4. 传多个依赖项
scss
useEffect(() => {
console.log("count 或 keyword 变化了");
}, [count, keyword]);
表示:
只要 count 或 keyword 中任意一个变化,副作用就会重新执行。
最通俗的理解方式
你可以把依赖数组理解成一句话:
只要数组里的这些值变了,就重新执行这段副作用代码。
这就很好记了。
十、useEffect 的清理函数是干嘛的?
很多人刚开始看到这种写法会有点懵:
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log("执行中");
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
为什么 useEffect 里面还要 return 一个函数?
这个函数叫:
清理函数
它一般会在这些时候执行:
- 组件卸载时
- 副作用重新执行前,先清理上一次的副作用
最常见的用途有:
- 清除定时器
- 移除事件监听
- 取消订阅
- 中断请求
比如监听窗口大小变化:
javascript
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
这里如果不做清理,组件销毁后事件还在,就可能造成内存泄漏或者逻辑混乱。
所以你可以这样记:
副作用用了什么外部资源,离开时就记得清掉。
十一、三个 Hook 的生活化比喻,一下就记住
为了让你更容易记住,我给你打个特别通俗的比方。
把 React 组件想象成一个办公室员工。
useState:员工的记事本
员工需要记住今天要做什么、当前完成多少、按钮是开还是关。
这些会影响工作展示给老板看。
所以:
useState = 会展示出来的正式数据
useEffect:员工的任务清单
员工上班后要做事:
- 给客户打电话
- 发邮件
- 开会
- 定时汇报
这些不是"展示内容",而是要执行的动作。
所以:
useEffect = 渲染后执行的额外任务
useRef:员工的抽屉
员工抽屉里放着一些东西:
- 钥匙
- 工牌
- 上一次会议记录
- 某个客户电话
- 临时编号
这些不需要写到汇报 PPT 上,但又得一直留着备用。
所以:
useRef = 持久保存但不驱动页面变化的数据容器
这个比喻基本能帮很多初学者彻底理顺。
十二、实际开发中该怎么选?
这里我给你一个非常实战的判断口诀。
场景一:数据变了,页面也要变
用 useState
比如:
- 输入框输入内容
- 列表数据变化
- loading 状态
- tab 切换
- 当前选中项
场景二:页面出来后要执行动作
用 useEffect
比如:
- 请求接口
- 绑定事件
- 启动定时器
- 修改标题
- 同步本地存储
场景三:只想记个值,不想刷新页面
用 useRef
比如:
- 保存 timer id
- 保存上一次值
- 防抖节流中的锁
- 获取 input DOM
- 防止重复提交标记
这个口诀非常适合业务开发时快速判断。
十三、一个综合案例,把三个 Hook 串起来理解
下面我们写一个小案例:搜索框自动聚焦,并在输入时同步标题,同时记录输入次数。
ini
import React, { useEffect, useRef, useState } from "react";
function SearchDemo() {
const [keyword, setKeyword] = useState("");
const inputRef = useRef(null);
const changeCountRef = useRef(0);
useEffect(() => {
inputRef.current.focus();
}, []);
useEffect(() => {
document.title = keyword ? `正在搜索:${keyword}` : "搜索页面";
}, [keyword]);
const handleChange = (e) => {
setKeyword(e.target.value);
changeCountRef.current += 1;
console.log("输入次数:", changeCountRef.current);
};
return (
<div>
<h2>搜索示例</h2>
<input
ref={inputRef}
value={keyword}
onChange={handleChange}
placeholder="请输入关键词"
/>
<p>当前关键词:{keyword}</p>
</div>
);
}
export default SearchDemo;
这个案例里:
useState
保存输入框内容 keyword
因为它要显示到页面上,所以必须用状态。
第一个 useEffect
页面加载后让输入框自动聚焦
因为这是渲染后执行的动作,所以用 useEffect。
第二个 useEffect
每当 keyword 变化时更新浏览器标题
这也属于副作用,所以还是 useEffect。
useRef
一个拿 DOM:inputRef
一个记录输入次数:changeCountRef
输入次数只是打印日志,并不展示到页面,所以没必要用 useState,用 useRef 更合适。
这个案例基本把三个 Hook 的职责划分得很清楚了。
十四、面试中怎么回答它们的区别?
如果面试官问你:
useState、useEffect、useRef 的区别是什么?
你可以这么回答:
useState 主要用于管理组件状态,当状态变化时会触发组件重新渲染,通常用来保存那些会影响页面展示的数据。
useEffect 主要用于处理副作用,也就是组件渲染之后需要执行的额外逻辑,比如请求接口、事件监听、定时器、修改标题等。
useRef 主要用于保存引用或者持久化数据,它既可以获取 DOM 元素,也可以保存一些不需要触发组件重新渲染的值,比如定时器 id、上一次的值等。
它们的核心区别在于:useState 管状态并驱动视图更新,useEffect 管副作用执行,useRef 管持久化引用但不会触发视图更新。
这段话很适合面试时直接说。
十五、初学者最常犯的几个错误
1. 该用 useRef 的地方用了 useState
比如只是存一个定时器 id,却写成:
scss
const [timer, setTimer] = useState(null);
其实这类数据不参与页面展示,用 useRef 更合理。
2. 该用 useState 的地方用了 useRef
比如页面上的数字要变化,却写成:
ini
const countRef = useRef(0);
countRef.current += 1;
结果发现页面不更新。
因为 useRef 的变化不会触发渲染。
3. 把所有逻辑都往 useEffect 里塞
有些逻辑其实只是普通计算,不一定非要写 useEffect。
不要一上来就觉得"只要是逻辑就放 useEffect"。
4. useEffect 依赖数组乱写
比如副作用里明明用到了 count,却不写到依赖数组里,容易造成旧值问题。
5. 忘记清理副作用
比如监听事件、开定时器却不清理,组件销毁后可能引发 bug。
十六、最后给你一个最简单的判断公式
以后开发时,如果你一时分不清到底该用谁,就套这三句判断。
第一问:这个数据要不要显示到页面上?
要,就优先考虑 useState
第二问:这个逻辑是不是要在渲染之后执行?
是,就优先考虑 useEffect
第三问:我是不是只是想记个值,或者拿 DOM,但不想刷新页面?
是,就优先考虑 useRef
这三个问题,基本能帮你解决 80% 的判断场景。
十七、总结
这篇文章讲了很多,其实最后你真正要记住的,就这几句话。
useState 是什么?
保存会影响页面展示的状态,状态变了会重新渲染。
useEffect 是什么?
处理渲染后的副作用,比如请求接口、事件监听、定时器等。
useRef 是什么?
保存不会触发重新渲染的值,或者获取 DOM 元素。
它们的最大区别是什么?
useState:存状态,更新会刷新页面useEffect:执行副作用,不是拿来存数据的useRef:存引用或值,但更新不会刷新页面
如果你之前一直觉得这三个 Hook 很绕,那你现在可以直接把它们理解成:
useState:页面数据管理员useEffect:页面行为执行器useRef:页面内部小仓库
这样再看 React Hook,很多东西就没那么抽象了。