大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
useEffect
这一课你将能做到
- 说出 Effect 适合做什么、不适合做什么
- 写出带清理函数的
useEffect(避免组件卸了还在 setState) - 看懂依赖数组
[]/[id]的含义
为什么要学
有些事发生在「画完界面之后」:改 document.title、订阅窗口大小、按课程 slug 去拉进度。
这些叫与外部系统同步,用 useEffect。
人话:React 负责画界面;世界(浏览器、后端、定时器)不在 JSX 里,要另开一个「画完再做、走时再收尾」的通道。
常见误用:用 Effect 把 props 再抄一份到 state,结果多跑很多遍、输入光标乱跳。能在渲染时算出来的,不要 Effect。
先建立图像
组件渲染到屏幕
↓
执行 Effect
↓
依赖变了 → 先 cleanup(清理)→ 再跑新的 Effect
组件卸载 → cleanup
依赖数组是「什么变了才值得再同步一次」。
跟着做
步骤 1:改标题(最小外部同步)
import { useEffect, useState } from "react";
export function TitleDemo() {
const [name, setName] = useState("同学");
useEffect(() => {
document.title = "你好," + name;
}, [name]); // name 变了才改标题
return (
<input value={name} onChange={(e) => setName(e.target.value)} />
);
}
document.title 不是 React 树的一部分,所以用 Effect。
步骤 2:请求 + 清理(本课最重要)
用户从课 A 很快切到课 B:A 的响应可能后到,把 B 的标题盖掉。要用旗标或 AbortController。
import { useEffect, useState } from "react";
export function TrackTitle({ slug }) {
const [title, setTitle] = useState("");
useEffect(() => {
let cancelled = false;
async function load() {
const res = await fetch("/api/tracks/" + slug);
const data = await res.json();
if (!cancelled) setTitle(data.title);
}
load();
return () => {
cancelled = true; // 已换 slug 或卸载:别再 setState
};
}, [slug]);
return <h1>{title || "加载中…"}</h1>;
}
人话:请求回来时,人可能已经走了。
步骤 3:空依赖 = 挂载后跑(开发模式可能两次)
useEffect(() => {
console.log("挂载后同步一次");
return () => console.log("卸载前清理");
}, []);
React 严格模式在开发期会故意重复挂载,帮你发现漏清理。生产不会这样双重调用副作用。看到两次 log 先别慌。
步骤 4:订阅要对上取消订阅
useEffect(() => {
function onResize() {
// 读窗口宽度等
}
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
只加不减会泄漏监听。定时器同样:clearInterval 配对。
步骤 5:清理订阅
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id);
}, []);
离开页面应停止 tick;否则泄漏。fetch 用 AbortController 同理。
讲清楚要点
- 能在渲染时计算的,不要 Effect(例如把 first+last 拼成 fullName)。
- 依赖数组要诚实:用到的 props/state 一般要写进去。漏了会「闭包旧值」:看起来用 id,实际一直请求第一门课。
- 清理订阅:
addEventListener配对removeEventListener。 - 不要用 Effect 响应点击:点击逻辑放事件处理函数。
- Next.js 里服务端取数可少写这类样板,但「过期响应 / 清理」思想相同。
你可能会懵的地方
-
开发模式跑两次?
严格模式故意如此。清理函数写对了就没事。 -
eslint 的 exhaustive-deps
听它。把函数放进 Effect 内,或包useCallback,不要随手注释掉规则。 -
把 props 同步进 state
多数是多余。直接渲染 props;要「可改副本」时再设计(受控/非受控)。 -
无限循环
Effect 里setState→ 渲染 → 依赖每次都是新对象 → 再 Effect。依赖写对象字面量最容易中招。 -
依赖数组漏了?
可能用到过期闭包。加依赖或证明刻意忽略并注释原因。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| props→state 无脑同步 | 多余渲染/打架 | 直接用 props |
| 无 cleanup 的请求 | 旧响应写回新页 | cancelled / AbortController |
| 在 Effect 里处理按钮逻辑 | 绕、难读 | onClick |
依赖写 [] 却用了 id | 永远拿第一次 | [id] |
| 无依赖却 setState 且无条件 | 死循环 | 看清依赖与更新条件 |
| 用 Effect 同步「可由 props 算出的值」 | 多余渲染 | 直接在渲染中计算 |
自测三问
- Effect 的清理函数什么时候跑?
[id]和[]差别?漏写 id 会怎样?- 举一个不该用 Effect 的例子(能渲染时算出来的)。
小结一句话
Effect 用来同步外部世界;写清依赖,返回清理函数,别拿它当万能生命周期筐。
动手加深
写:挂载拉课表 → loading → setRows;卸载 abort。对照 react.dev Effect 文档「你可能不需要 Effect」。
跟着做(加厚 · react-effects)
步骤:最小可运行对照
在 Vite 或 Next 练习页粘贴本课核心示例,改一个输入,确认屏幕变化符合预期。再故意写一个错法表里的写法,看控制台/界面如何坏,然后改回。
每个概念都问:state 在谁手里?UI 哪一块会因它重绘?
操作顺序:打开对应文件 → 改一处可观察点(文案、类型、className 或分支)→ 保存看结果 → 复原或留下有意义的小改动。
步骤:错法故意练一次
先按「用 jQuery 式直接改 DOM 冒充完成」做 30 秒(或在注释里写出若这样做会怎样),再改成「改数据(state/props)让 UI 推导出来」。 目的不是留坑,而是让失败现象进长期记忆。
你可能会懵的地方(加厚)
-
热更新后状态丢了?
正常:整树重挂会重置 state。用 key 或外提状态保留;开发期别被吓到。 -
和文档示例不一致?
以你锁的 React 大版本 + react.dev 为准;旧博客的 class 组件示例仅作历史阅读。 -
这课和前后课怎么接?
回看大纲上一课的小结一句、下一课标题;用自己的话写「本课产出什么可观察结果」。写不出来就重做跟着做。
错法对照(加厚)
| 错法 | 问题 | 正解 |
|---|---|---|
| 用 jQuery 式直接改 DOM 冒充完成 | 假完成/埋雷 | 改数据(state/props)让 UI 推导出来 |
| 只看不敲 | 第二天全忘 | 至少跑通本课步骤 |
| 跳过自测三问 | 以为会了 | 闭卷答再对稿 |
延伸
- react.dev Synchronizing with Effects
- 下一课:useRef——不触发渲染的「盒子」。