Appearance
Hooks 基础:state、effect、ref、context
Hooks 是现代 React 的主线。
但真正困难的地方不是“记住 Hook 名字”,而是:
- 每个 Hook 解决的到底是哪类问题
- 它和渲染过程是什么关系
- 为什么
useEffect、闭包旧值、依赖数组总是容易出问题
这篇文章重点讲:
- Hooks 为什么会出现
- Hooks 的使用规则是什么
useState、useEffect、useRef、useContext各自做什么useEffect为什么最容易写出问题
1. Hooks 为什么会出现
在函数组件成为主流之后,React 需要一套方式让函数组件也能承载:
- 本地状态
- 副作用
- 生命周期式能力
- 逻辑复用
Hooks 就是在解决这件事。
Hooks 让函数组件不只是“接收 props 返回 JSX”,也能管理状态、处理副作用并复用逻辑。
2. Hooks 的两条基本规则
这两条非常重要。
2.1 只在最外层调用 Hook
不要放在:
iffor- 条件分支内部
- 普通嵌套函数内部
原因不是“语法不喜欢”,而是 React 需要稳定的 Hook 调用顺序。
2.2 只在 React 函数组件或自定义 Hook 里调用 Hook
普通工具函数里不能随便调用 Hook。
🌟 这两条规则的核心,不是背规范,而是 React 需要根据组件每次渲染时相同的 Hook 顺序,来把状态槽位对应起来。
3. useState 到底在解决什么问题
useState 用来给函数组件增加本地状态。
例如:
jsx
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}这里最关键的点是:
count是当前渲染看到的状态值setCount不是直接改变量,而是发起一次状态更新- 状态变化后,组件会进入下一轮渲染
3.1 什么时候用函数式更新
当你的新状态依赖旧状态时,更稳妥的写法通常是:
jsx
setCount(prev => prev + 1);这能避免:
- 闭包读到旧值
- 多次更新被同一轮旧值覆盖
4. useEffect 到底在解决什么问题
useEffect 处理的是副作用。
副作用可以看成,那些不属于纯渲染计算,但又需要在组件运行过程中做的事情。
常见场景包括:
- 发请求
- 订阅事件
- 手动操作外部系统
- 定时器
- 日志和埋点
例如:
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
async function loadUser() {
const data = await fetch(`/api/users/${userId}`).then(res => res.json());
if (!cancelled) {
setUser(data);
}
}
loadUser();
return () => {
cancelled = true;
};
}, [userId]);
if (!user) {
return <div>loading...</div>;
}
return <div>{user.name}</div>;
}这段代码最值得注意的是:
- effect 依赖了
userId - 依赖变化时会重新执行
- 返回清理函数,避免旧请求结果继续污染新状态
5. 依赖数组到底怎么理解
依赖数组不是“随便填让 ESLint 不报错”的东西。
它真正表达的是,这个 effect 依赖哪些渲染时值,只要这些值变化,就应该重新同步副作用。
常见情况:
| 写法 | 含义 |
|---|---|
| 不传依赖数组 | 每次渲染后都执行 |
[] | 通常表示挂载后执行一次 |
[a, b] | a 或 b 变化时重新执行 |
🌟 依赖数组不是“控制 effect 什么时候跑”的开关,而是“声明 effect 依赖了哪些值”的清单。
6. 为什么 useEffect 最容易出问题
因为它天然跨越了两套世界:
- React 渲染世界
- 外部副作用世界
最常见的问题通常是:
- 依赖没写全
- 清理没写好
- 把太多逻辑塞进一个 effect
- 读到了旧闭包里的值
所以更自然的写法通常是:
- 一个 effect 只做一类同步动作
- 依赖显式写全
- 能放在事件回调里的逻辑,就不要硬塞进 effect
7. 闭包旧值问题怎么理解
这是 React 里非常常见、也特别容易把人绕进去的问题。
原因可以压成这 3 条:
- 函数组件每次渲染都会创建新的作用域
- 回调和 effect 捕获的是当次渲染时的变量快照
- 后续状态变了,但旧回调没更新,就会读到旧值
例如:
jsx
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount(prev => prev + 1)}>{count}</button>;
}这里 setInterval 里拿到的 count,通常一直是第一次渲染时的值。
常见解决方式:
- 补全依赖
- 用函数式更新
- 用
useRef保存最新值
8. useRef 到底在解决什么问题
useRef 最常见的两类用途是:
- 拿 DOM 引用
- 保存不会触发重新渲染的可变值
8.1 DOM 引用
jsx
function SearchInput() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
</>
);
}8.2 保存最新值
jsx
function AutoSave({ value }) {
const latestValueRef = useRef(value);
useEffect(() => {
latestValueRef.current = value;
}, [value]);
useEffect(() => {
const id = setInterval(() => {
console.log("save", latestValueRef.current);
}, 5000);
return () => clearInterval(id);
}, []);
}这里 useRef 的价值是,让某个值跨渲染保留下来,但又不因为变化而触发重新渲染。
9. useContext 到底在解决什么问题
useContext 用来读取 Context 里的共享上下文。
它最适合解决的是,跨很多层组件传相同数据时,不想层层透传 props。
例如:
jsx
const ThemeContext = createContext("light");
function Page() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div>theme: {theme}</div>;
}它适合:
- 主题
- 当前登录用户
- 语言信息
- 配置上下文
但它不等于完整状态管理框架。
10. 基础 Hooks 最容易混的边界
| Hook | 更适合做什么 |
|---|---|
useState | 组件本地状态 |
useEffect | 副作用同步 |
useRef | DOM 引用、可变值容器 |
useContext | 读取共享上下文 |
如果把它们用一句话串起来:
useState管内部状态useEffect管副作用useRef管不参与渲染的引用和值useContext管跨层共享上下文
11. 一句话总结
React 基础 Hooks 的主线,本质上是在解决 函数组件如何拥有状态、如何同步副作用、如何跨渲染保存引用,以及如何读取共享上下文。