5. Hooks:useState 与 useEffect
这是什么:Hook = 在函数组件里「借」React 能力的函数,useState 借记忆,useEffect 借副作用。
为什么用:上一章只能在渲染过程中算数据,但改页面标题、开定时器、请求数据这些「组件之外的事」必须另外找地方做,Hook 正是干这个的。
如何用:本章写一个「可停的自动计数器」感受副作用与清理,再用空依赖数组做一次挂载时的网络请求。
5.1 useEffect:可停的自动计数器
副作用与清理函数一页打通(改页面标题 + 定时器 + 清理),整段可跑:
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
<script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
<script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
<div id='root'></div>
<script type='text/babel' data-presets='react'>
function Timer() {
const [count, setCount] = React.useState(0)
const [running, setRunning] = React.useState(false)
React.useEffect(() => {
document.title = `已点 ${count} 次` // 副作用:改外部世界(页面标题)
}, [count]) // 依赖数组:count 变了才执行
React.useEffect(() => {
if (!running) return // 停止状态:不挂定时器
const timer = setInterval(() => {
setCount((c) => c + 1) // 新值依赖旧值 → 永远传函数
}, 500)
return () => clearInterval(timer) // 清理:停止或卸载时打扫
}, [running])
return (
<div>
<h2>计数:{count}</h2>
<button onClick={() => setRunning(!running)}>
{running ? '停止' : '开始'}
</button>
<p>浏览器标签上的标题也在跟着变——那个就是「页面外」的副作用</p>
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<Timer />)
</script>
</body>
</html>
你会看到:点「开始」每半秒自动加一,浏览器标签标题同时跟着变;点「停止」立刻打住——干净利落,全靠那个 return 的清理函数。
读法:「当 [ ] 里的依赖变化后,执行箭头函数」。
5.2 依赖数组的三种形态(必背)
| 写法 | 执行时机 | 用途 |
|---|---|---|
useEffect(fn) |
每次渲染后都执行 | 几乎不这么用 |
useEffect(fn, []) |
只在挂载后一次 | 初始拉数据、订阅 |
useEffect(fn, [a, b]) |
a 或 b 变了就执行 | 联动副作用 |
看到
[]想到「onMounted」(Vue 的 onMounted 同学应心领神会)。
5.3 挂载时请求一次:真实网络请求
空数组 + fetch 的实战场,整段可跑(数据来自 npmmirror 的公开 registry 接口,与本章 CDN 同源体系):
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
<script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
<script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
<div id='root'></div>
<script type='text/babel' data-presets='react'>
function PackageCard() {
const [pkg, setPkg] = React.useState(null)
React.useEffect(() => {
fetch('https://registry.npmmirror.com/@babel/standalone/7.29.11')
.then((res) => res.json())
.then((data) => setPkg(data)) // 异步回来再 set,页面自动更新
}, []) // 空数组:挂载时请求一次
return (
<div>
<p>npmmirror 实时数据:</p>
{pkg && (
<ul>
<li key='1'>包名:{pkg.name}</li>
<li key='2'>版本:{pkg.version}</li>
<li key='3'>简介:{pkg.description}</li>
</ul>
)}
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<PackageCard />)
</script>
</body>
</html>
你会看到:页面先只有「npmmirror 实时数据:」一行,一两秒后长出包名、版本号和简介——请求是异步的,数据回来 set 一下,视图自动跟上。
5.4 常见坑
- 死循环:deps 里漏依赖 → 状态引用自己 → 反复渲染。解决:写全依赖
- 对象/数组依赖:引用每渲染一次都变 → 视为「变了」无限循环。解决:拆到基本值
- 不该把「计算」放 useEffect:能由数据直接算出(Vue 里的 computed 一样)的交给渲染期直接计算或
useMemo
5.5 动手改
- 给 5.1 的 Timer 把间隔改成 1000;再把「开始/停止」逻辑再读一遍,说出清理函数在哪一刻执行
- 把每次 count 更新写入 localStorage(useEffect + deps [count])
- 把 5.3 接口地址里的
7.29.11换成18.3.1所在的另一个包路径(如react/18.3.1),看卡片数据怎么变
5.6 小结
- useEffect 管副作用,deps 决定时机:每次 / 一次 / 按依赖
- 会泄漏的副作用记得 return 清理函数(5.1 的 setInterval 就是最好的例子)
- 更新值依赖旧值时用「函数式 setState」
setX((v) => ...);派生数据别放 effect
下一章:列表渲染与条件渲染 🐸