React Hooks 保姆级教程:把组件想象成失忆的打工人,4 个 Hook 带你原地起飞
💡 本文面向 React 初学者,用生活化比喻讲透
useState、useEffect、useRef、useContext和自定义 Hook。读完后你会彻底告别"背了又忘、忘了又背"的死循环。
一、先聊点让人血压升高的往事
还记得 Class 组件的时代吗?一个简单的计数器,你得这么写:
jsx
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this); // 😭 还得手动 bind
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<button onClick={this.handleClick}>
点击了 {this.state.count} 次
</button>
);
}
}
一个计数器,用了 15 行代码,还牵扯到 constructor、this、bind、render 一堆概念。对小白来说,this 指向问题就是第一道天堑------你永远不知道哪个 this 会突然"离家出走"。
而 Hooks 的出现,就是来拯救我们的。
同样的计数器,Hooks 版本只有 6 行:
jsx
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>
);
}
没有 this,没有 bind,没有 class。函数组件 + Hooks,就是现代 React 的标准姿势。
那 Hooks 到底是什么?别急,我给你讲一个故事 👇
二、核心比喻:函数组件 = 一个「失忆的打工人」
理解 Hooks 的钥匙,就藏在一句话里:
函数组件每次渲染,都是把整个函数从头到尾重新执行一遍。
想象你是一个记性极差的外卖骑手(函数组件)。老板(React)每次喊你"重新跑一趟"(触发渲染),你就会把上一单的事忘得一干二净,从第一行开始重新执行。
问题来了:上一单送到哪了?客户要什么?你得记下来啊!
于是你需要几个「工具」来对抗失忆,这些工具就是 Hooks:
| Hook | 比喻 | 一句话作用 |
|---|---|---|
useState |
📝 便利贴 | 记住需要触发更新的数据 |
useEffect |
🧹 下班后的家务 | 渲染后去做"副作用" |
useRef |
🔒 私房钱抽屉 | 存东西,但"不声张"(不触发渲染) |
useContext |
📢 公司广播喇叭 | 跨层级共享数据,不用层层传话 |
下面我们一个个拆开讲。
三、useState:给失忆的组件贴上「便利贴」
1. 为什么需要它
函数组件里,你写的普通变量是"一次性"的:
jsx
function BadCounter() {
let count = 0; // ❌ 每次渲染都会被重置为 0
return (
<button onClick={() => {
count++; // 虽然改了,但界面根本不会变
console.log(count);
}}>
点击了 {count} 次
</button>
);
}
点一下按钮,控制台打印 1,但界面上的数字纹丝不动。为什么?
因为:① 普通变量每次渲染都被重置;② 就算没被重置,React 根本不知道你改了变量,它不会主动重新渲染。
useState 就是来解决这两个问题的:
jsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// ↑当前值 ↑修改函数 ↑初始值
return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>
);
}
useState(0):告诉 React"帮我记一个值,初始是 0";count:当前的值(React 帮你保存,下次渲染不会丢);setCount(新值):修改值,并通知 React 重新渲染。
2. 一个很关键的坑:更新是"异步"的
先看这段代码,猜猜打印什么?
jsx
function Demo() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 猜猜这里是 0 还是 1?
};
return <button onClick={handleClick}>点击</button>;
}
答案是 0。
因为 setCount 并不会立刻改掉 count,它只是"预约"了一次更新,等到下次渲染时 count 才会变成新值。当前这次函数执行里,count 还是老样子。
所以如果你要"基于上一个值做计算",别用 count,要用函数式更新:
jsx
// ❌ 连续调两次,结果只加 1(因为两次拿到的 count 都是旧值)
setCount(count + 1);
setCount(count + 1);
// ✅ 函数式更新:连续加 2,正确!
setCount(c => c + 1);
setCount(c => c + 1);
💡 记住一句话:「算新值要用旧值时,传函数」。
3. 对象和数组:必须"换新的",不能"改原值"
很多小白会在这一步翻车:
jsx
const [user, setUser] = useState({ name: '小明', age: 18 });
// ❌ 错误:直接改原对象,React 检测不到变化
user.age = 19;
setUser(user);
// ✅ 正确:用展开运算符造一个新对象
setUser({ ...user, age: 19 });
// 数组同理
const [list, setList] = useState([]);
// ✅ 不要 push,要返回新数组
setList([...list, newItem]);
原理很简单:React 判断"要不要更新"用的是 引用比较 。你改的是同一个对象的内部属性,引用没变,React 就认为"没变",直接跳过。造一个新的引用,它才会动。
四、useEffect:渲染之后,顺手做的「家务活」
1. 什么是"副作用"
先搞懂这个词,你就懂一半了。
"副作用"(Side Effect)= 组件渲染之外,还要对"外部世界"做的事,比如:
- 请求接口数据(
fetch) - 操作浏览器 API(改标题、存 localStorage)
- 订阅/监听(
addEventListener、setInterval) - 手动改 DOM
这些事不能在渲染过程中做(渲染要"纯"),所以 React 给了你一个时机:渲染完成之后 。这个时机,就由 useEffect 提供。
2. 基本用法
jsx
import { useState, useEffect } from 'react';
function UserInfo({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 渲染完成后执行:请求数据
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]); // 👈 依赖数组:userId 变化时才重新执行
if (!user) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
3. 依赖数组的三种写法(面试必考)
jsx
// ① 不传第二个参数:每次渲染都执行
useEffect(() => { ... });
// ② 传空数组 []:只在「首次渲染后」执行一次
useEffect(() => {
console.log('我只在出生时执行一次');
}, []);
// ③ 传 [a, b]:首次执行 + a 或 b 变化时执行
useEffect(() => {
console.log('a 或 b 变了');
}, [a, b]);
4. 清理函数:离开前记得"善后"
有些副作用需要"撤销",否则会内存泄漏。比如定时器、事件监听、订阅:
jsx
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => setSeconds(s => s + 1), 1000);
// ✅ 返回清理函数:组件卸载(或下次执行前)会调用
return () => clearInterval(id);
}, []);
return <div>已经过去 {seconds} 秒</div>;
}
💡 记住:
useEffect里"造"了什么,return里就"拆"掉什么,一一对应。
5. 两个高频踩坑
坑一:无限循环
jsx
// ❌ 危险!每次渲染都会 setUser → 触发重新渲染 → 又执行 effect → 死循环
useEffect(() => {
setUser({ name: '小明' });
}); // 没写依赖数组
坑二:闭包陷阱(拿不到最新的 state)
jsx
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 永远打印 0,因为闭包锁住了首次的 count
}, 1000);
return () => clearInterval(id);
}, []);
解决办法还是那招------用函数式更新 或把 count 加进依赖数组。
五、useRef:一个「不声张」的私房钱抽屉
useRef 和 useState 都能"存值",但有一个关键区别,这是面试官最爱问的点:
useState |
useRef |
|
|---|---|---|
| 改了之后触发渲染? | ✅ 会 | ❌ 不会 |
| 渲染后值还在吗? | ✅ 在 | ✅ 在 |
| 典型用途 | 驱动 UI 变化 | 存"不需要渲染"的值 |
一句话总结:
useState是"把东西贴在脑门上",一改就嚷嚷着要重新渲染;useRef是"把东西塞进抽屉",改了也默不作声。
1. 用途一:操作 DOM 元素
这是 useRef 最常见的用法------拿到某个 DOM 节点,手动操作它:
jsx
import { useRef } from 'react';
function InputFocus() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>
点我聚焦输入框
</button>
</>
);
}
inputRef.current 就是那个真实的 <input> DOM 节点,你可以直接调它的 .focus() 等方法。
2. 用途二:存一个"改了但不想渲染"的值
比如记录"上一次的值"、存一个计时器 id、存一个不会变的配置:
jsx
function ClickLogger() {
const clickCount = useRef(0); // 记录点击次数,但不触发渲染
return (
<button onClick={() => {
clickCount.current++;
console.log('实际点了', clickCount.current, '次');
}}>
点我(界面不会重渲染)
</button>
);
}
六、useContext:公 司广播喇叭,告别层层传话
1. 痛点:props 的"快递中转站"
组件嵌套一深,传数据就成了噩梦,这叫 Prop Drilling(属性钻取):
jsx
<App> // 主题 = "dark"
<Header theme="dark"> // 中转 1
<Nav theme="dark"> // 中转 2
<Button theme="dark" /> // 真正要用,却要经过 3 层
中间那些组件根本不用 theme,却被迫当"快递员",又丑又难维护。
2. 解法:Context = 公司全员群
useContext 的思路是:数据放到一个"广播喇叭"(Provider)里,谁需要谁直接"收听"(useContext),不用层层转发。
jsx
import { createContext, useContext } from 'react';
// ① 创建一个"广播频道",默认值是 'light'
const ThemeContext = createContext('light');
function App() {
return (
// ② Provider = 打开喇叭,广播 value
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// 中间组件 Toolbar 完全不碰 theme,也无需透传
function Toolbar() {
return <ThemedButton />;
}
// ③ 真正需要的组件,用 useContext 直接"收听"
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>当前主题:{theme}</button>;
}
注意三个步骤:createContext 建频道 → Provider 广播 → useContext 收听,缺一不可。
3. 适用场景
- 主题、语言、登录用户等"全局状态"
- 但别滥用:数据频繁变化时,Context 的更新会让所有订阅者重渲染,性能不如专门的全局状态库(如 Zustand、Redux)
七、自定义 Hook:你的「逻辑乐高积木」
学到这,你已经会用内置 Hook 了。但真正让你"进阶"的,是自定义 Hook。
1. 它解决什么问题
组件写多了你会发现:很多逻辑是重复的。比如"存 localStorage"这个需求,你可能会在 5 个组件里写 5 遍几乎一样的代码。
自定义 Hook 就是把「一段包含状态的逻辑」打包成一个函数,哪里需要哪里调,就像拼乐高。
2. 唯一铁律:以 use 开头
js
function useXxx(...) { ... } // ✅ 合法
function xxx(...) { ... } // ❌ React 会报错
为什么要 use 开头?因为 React 靠这个名字来识别"这是一个 Hook",从而做规则检查(比如不能在条件语句里调 Hook)。
3. 实战一:useLocalStorage
把"读和写 localStorage"打包:
jsx
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 惰性初始化:只在第一次渲染时读 localStorage
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved !== null ? JSON.parse(saved) : initialValue;
});
// 值一变,就自动写回 localStorage
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue]; // 对外暴露的接口和 useState 一模一样
}
// 使用:跟 useState 一样顺手,但数据会自动持久化
function App() {
const [name, setName] = useLocalStorage('name', '小明');
return (
<input value={name} onChange={e => setName(e.target.value)} />
);
}
刷新页面,输入框里的名字还在------因为已经悄悄存进了 localStorage。
4. 实战二:useDebounce(防抖)
搜索框场景几乎必用,输入时别每次都发请求:
jsx
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer); // 每次变化都清掉上一个定时器
}, [value, delay]);
return debounced;
}
// 使用:用户停下输入 500ms 后,debouncedSearch 才是最终值
function SearchBox() {
const [text, setText] = useState('');
const debouncedText = useDebounce(text, 500);
useEffect(() => {
if (debouncedText) {
console.log('发起搜索:', debouncedText);
}
}, [debouncedText]);
return <input value={text} onChange={e => setText(e.target.value)} />;
}
5. 实战三:useWindowSize(监听窗口大小)
jsx
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () =>
setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize); // 记得清理
}, []);
return size;
}
八、一张表,收下全部重点
| Hook | 解决什么问题 | 核心记忆点 |
|---|---|---|
useState |
组件要"记住"并更新数据 | 更新是异步的;对象/数组要造新引用 |
useEffect |
渲染后做副作用 | 依赖数组三种写法;记得写清理函数 |
useRef |
存不触发渲染的值 / 操作 DOM | 改 .current 不会重新渲染 |
useContext |
跨层级共享数据 | 建频道 → 广播 → 收听 |
| 自定义 Hook | 复用带状态的逻辑 | 必须 use 开头;本质就是个函数 |
九、写在最后
回顾一下这条主线,你会发现 Hooks 一点都不玄:
函数组件是个记性差的打工人 ,
useState给它贴便利贴、useEffect让它下班后做家务、useRef给它藏私房钱的抽屉、useContext是公司的广播喇叭------而自定义 Hook,就是把这几样工具打包成你自己的"百宝箱"。
这 4 个内置 Hook 涵盖了日常开发 90% 的场景。先把它们用熟,再去看 useMemo、useCallback、useReducer 这些进阶货,你会轻松很多。
📝 本文基于 React 16.8+ 的核心 Hook,适用于 React 18 / 19。 🔗 官方文档:react.dev/reference/r...