React Hooks深入
写给初学者:每个概念都从”为什么需要它”开始讲,配合通俗比喻和完整可运行代码。
1.1 为什么「连点三次 +1」可能只加了 1?
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // ❌ 三行都等价于 setCount(0 + 1)
setCount(count + 1); // 因为 handleClick 执行那瞬间 count 还是 0
setCount(count + 1);
}
// 结果:count = 1,不是 3
}通俗比喻:你让三个人分别”把仓库里的苹果数量 +1 并存回去”。但三个人看的是同一张老清单(写着 0),都以为现在有 0 个,都存了 1 回去——结果仓库还是 1 个。
1.2 函数式更新
给 setCount 传一个函数,React 会用”它内部最真实的最新值”来调用:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(prev => prev + 1); // ✅ prev 是 React 给的最新值
setCount(prev => prev + 1); // ✅ 这次的 prev 已经是 1
setCount(prev => prev + 1); // ✅ 这次的 prev 已经是 2
}
// 点击一次 → count: 0 → 3 ✅
}什么时候用函数式更新:
- 新值无关旧值:
setCount(0)、setName(input) - 新值基于旧值:
setCount(prev => prev + 1)
1.3 对象状态——必须整个换掉
// ❌ 直接改属性——React 不会重新渲染!
user.age = user.age + 1;
// ✅ 创建一个新对象
setUser({ ...user, age: user.age + 1 });
// 函数式更新版本(更稳)
setUser(prev => ({ ...prev, age: prev.age + 1 }));
// 注意:箭头函数返回对象要加括号 () => ({ ... })通俗比喻:React 是个只认收据的仓库管理员。你必须交一张新的入库单(
{...user, age: 26}),管理员才肯重新盘点。
1.4 数组状态——同样要换新数组
| 想做的事 | 写法 | Python 对照 |
|---|---|---|
| 末尾加一个 | [...arr, item] | arr + [item] |
| 开头加一个 | [item, ...arr] | [item] + arr |
| 删掉下标 i 的 | arr.filter((_, idx) => idx !== i) | 列表推导 |
| 合并两数组 | [...a, ...b] | a + b |
1.5 初值的惰性初始化
// ⚠️ 每次渲染都会执行 computeInitial(),浪费
const [data, setData] = useState(computeInitial());
// ✅ 传函数本身,React 只在第一次渲染时调用
const [data, setData] = useState(() => computeInitial());
// 实际场景——从 localStorage 读初始值
const [name, setName] = useState(() => {
return localStorage.getItem("myName") || "匿名";
});一、useState 的三条铁律
铁律 1:只在组件顶层调用,不能放进 if/循环/函数里
function Good({ isVip }) {
// ✅ 所有 useState 都在最顶层
const [a, setA] = useState(0);
const [b, setB] = useState(isVip ? 1 : 0);
}React 内部用数组按顺序存每个 useState 的值,靠”调用顺序”对应。放进 if 里会导致错位。
通俗比喻:像在食堂打饭按顺序领餐盘。你今天插队少领一个,后面所有人拿到的餐盘就全是别人的。
铁律 2:不可变更新——永远创建新值,不直接改老值
// ❌ 直接改
user.age = 26; setUser(user);
todos.push("x"); setTodos(todos);
// ✅ 生成新值
setUser({ ...user, age: 26 });
setTodos([...todos, "x"]);铁律 3:拆分状态,别把所有东西塞一个对象
// ❌ 把无关的状态塞一坨
const [form, setForm] = useState({ name: "", age: 0, loading: false, error: null });
// ✅ 无关的状态分开
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const [loading, setLoading] = useState(false);
// 例外:总是一起变化的放一起
const [pos, setPos] = useState({ x: 0, y: 0 });二、受控组件
受控组件 = 输入框的值由 React 状态控制,用户输入立刻同步到状态。
// input
function NameInput() {
const [name, setName] = useState("");
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<p>你好, {name}!</p>
</div>
);
}
// checkbox(注意是 checked 不是 value)
<input
type="checkbox"
checked={agreed}
onChange={e => setAgreed(e.target.checked)}
/>
// select
<select value={color} onChange={e => setColor(e.target.value)}>
<option value="red">红色</option>
<option value="blue">蓝色</option>
</select>
// textarea(React 统一用 value 属性)
<textarea value={bio} onChange={e => setBio(e.target.value)} />受控组件统一定律:不管哪种表单元素,都是「一个状态 × value/checked 属性 × onChange 同步」。
表单提交时务必
e.preventDefault()阻止页面刷新。
1.6 基本结构
useEffect(() => {
// 副作用代码(渲染"完"之后执行)
return () => {
// 清理函数(组件消失时、或下次 effect 执行前调用)
};
}, [依赖1, 依赖2]);三要素:
- 副作用函数:干正事(发请求、设定时器)
- 清理函数:收拾烂摊子(清定时器、取消订阅)
- 依赖数组:告诉 React”什么变了要重新执行”
1.7 四种使用模式
| 模式 | 写法 | 用途 |
|---|---|---|
| 只跑一次 | useEffect(fn, []) | 初始加载、设全局定时器 |
| 依赖变化才跑 | useEffect(fn, [id]) | 依赖某些值的副作用 |
| 每次渲染都跑 | useEffect(fn) | 几乎不用 |
| 带清理 | return () => clearInterval(t) | 定时器/订阅 |
1.8 完整的异步数据获取模式(实战必背)
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function load() {
try {
setLoading(true);
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data: User[] = await res.json();
setUsers(data);
} catch (e) {
setError(e instanceof Error ? e.message : "未知错误");
} finally {
setLoading(false);
}
}
load();
}, []);
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了: {error}</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}这个”loading / error / data 三态 + try/catch/finally + 包 async 函数”模式,是 React 里最常见的数据获取写法。
速查表
| 概念 | 一句话 | 关键代码 |
|---|---|---|
| 函数式更新 | 基于”最新值”计算新值 | setCount(prev => prev + 1) |
| 对象状态 | 必须整个换新对象 | setUser({...prev, age: 26}) |
| 受控 input | value 受状态控制 | <input value={t} onChange={...} /> |
useEffect | 副作用(请求/定时器/订阅) | useEffect(fn, deps) |
| 数据获取三态 | loading/error/data | useState(true)/useState<null>(null) |
速记卡(面试闪卡)
Q1:一句话讲清「React Hooks深入」到底是什么? A:React Hooks 让函数组件也能管状态跑副作用:useState 记数据、useEffect 管请求/定时器,但都靠”调用顺序”和”不可变更新”两条铁律。
Q2:useState 为什么连点三次只加 1 —— 怎么理解?
A:像三个人看同一张写着 0 的老清单,都以为现在 0 个、各存回 1——仓库还是 1。根因:三个 setCount(count+1) 用的都是点击瞬间那个旧 count。解法传函数 setCount(prev => prev+1),React 会给”它内部最新的真实值”,三次就真加到 3。这就是函数式更新。
Q3:useState 的三条铁律 —— 怎么理解?
A:① 只在顶层调用——React 用数组按”顺序”存每个 Hook 的值,塞进 if 里会错位(像食堂打饭插队少领一个,后面全拿错餐盘);② 不可变更新——永远建新值(setUser({...user, age:26})),直接改属性 React 不重渲染;③ 拆分状态,无关的别塞一坨对象。
Q4:useEffect 怎么用 —— 怎么理解?
A:useEffect 像”渲染完后才开工的清洁工”:副作用函数干正事(发请求、设定时器),return 的清理函数收烂摊子(清定时器、取消订阅),依赖数组告诉它”什么变了才重跑”。四种模式:只跑一次 []、依赖变才跑 [id]、每次都跑(几乎不用)、带清理。异步数据获取必背 loading/error/data 三态。
Q5:受控组件是什么 —— 怎么理解?
A:受控组件 = 输入框的值由 React 状态说了算,用户输入立刻同步回状态。统一公式就一句:一个状态 × value/checked 属性 × onChange 同步——input 用 value、checkbox 用 checked、select/textarea 同理。提交时记得 e.preventDefault() 拦掉页面刷新。
Q6:核心速记主线有哪些?
- useState:函数式更新用 prev,对象/数组要换新
- 三条铁律:顶层调用、不可变、拆状态
- useEffect:副作用+清理+依赖数组,异步三态
- 受控组件:状态控 value,onChange 同步
口诀 A:Hooks 函数组件管状态,顺序存值别乱排 连点三次只加一,函数更新 prev 来 不可变更新建新值,顶层调用莫塞 if useEffect 收烂摊,受控一态一同步
相关链接
- 目录:00-TypeScript
- 上一篇:JSX与组件
- 下一篇:07-React副作用与自定义Hook