React Hooks
React Hooks 的整体分工、使用边界与常见模式总览。
[!info] related notes
- 所属 MOC: React MOC
- 上位主题: react
- 重点主题: react-use-effect, react-use-layout-effect, react-use-state, react-use-ref, react-use-context, react-use-reducer, react-use-memo, react-use-callback, react-memo, react-custom-hooks
- 前置知识: jsx, javascript
React Hooks
Hooks 让函数组件拥有状态、上下文、副作用、引用和值缓存等能力。它们不是一组零散 API,而是 React 组件能力的统一入口。
Hooks 也是 React 对组件逻辑组织方式的一次升级:它让函数组件拥有“状态、生命周期、副作用、上下文、性能缓存、可复用逻辑”这些能力。
为什么 Hooks 重要
- 让函数组件取代大多数类组件场景
- 让状态逻辑和副作用逻辑可以抽成自定义 Hook 复用
- 让组件更容易按功能组织,而不是按生命周期切碎
Hooks 到底解决了什么
在 Hooks 之前:
- 同一件事常被拆到
componentDidMount、componentDidUpdate、componentWillUnmount - 复用逻辑经常要靠 HOC 或 render props
- 函数组件更轻,但能力不完整
Hooks 把“状态 + 副作用 + 逻辑组合”带回了函数组件,让你可以按功能聚合逻辑,而不是被生命周期 API 牵着走。
Hooks 的底层心智模型
组件每次渲染时,函数都会重新执行。React 之所以还能知道“哪个 state 属于哪个 Hook”,依赖的不是变量名,而是 Hook 的调用顺序。
可以把它先粗略理解成:
- 第 1 个 Hook 槽位
- 第 2 个 Hook 槽位
- 第 3 个 Hook 槽位
所以这段代码里:
function Counter() {
const [count, setCount] = useState(0)
const [name, setName] = useState('A')
}
React 真正依赖的是:
- 第 1 个
useState对应count - 第 2 个
useState对应name
可以把 Hooks 分成四组理解
状态类
useStateuseReducer
它们负责让组件记住会影响渲染的值。
进一步看:react-use-state, react-use-reducer
副作用类
useEffectuseLayoutEffect
它们负责和 React 外部系统同步,而不是单纯“渲染后顺手做点事”。
进一步看:react-use-effect, react-use-layout-effect
引用与上下文类
useRefuseContext
它们分别解决“不参与渲染的可变值”和“跨层共享值”的问题。
进一步看:react-use-ref, react-use-context
性能与体验类
useMemouseCallbackuseTransitionuseDeferredValue
它们主要用于优化重计算、引用稳定性和交互优先级。
进一步看:react-use-memo, react-use-callback
最重要的两条规则
- 只在 React 函数组件或自定义 Hook 中调用 Hook
- 只在顶层调用,不要放进条件、循环或嵌套函数里
原因不是风格偏好,而是 React 依赖 Hook 的调用顺序来匹配状态槽位。
错误例子:
function Bad({ visible }: { visible: boolean }) {
if (visible) {
useEffect(() => {
console.log('shown')
}, [])
}
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
当 visible 一会儿是 true,一会儿是 false 时,Hook 顺序就乱了。
为什么名字都要以 use 开头
这不只是命名风格。
- 它告诉团队和读代码的人:这是 Hook,不是普通工具函数
- 它让 ESLint 和工具链知道:这里要遵守 Hook 规则
- 它意味着这个函数内部可能继续调用其他 Hook
自定义 Hook 的价值
自定义 Hook 复用的是“状态 + 副作用 + 处理逻辑”,而不是直接复用 UI 结构。
常见例子有:useFetch、useToggle、useDebounce、usePrevious。
进一步看:react-custom-hooks
Hooks 学习里真正难的点
真正难的不是记住 API 名字,而是搞懂:
- 为什么 Hook 只能顶层调用
- 为什么每次渲染都会形成新闭包
- 为什么
useEffect不是“渲染后随便写逻辑” - 为什么依赖数组必须真实反映依赖
- 为什么
useMemo/useCallback不是默认必加
一套选择 Hook 的思维框架
- 需要让 UI 记住某个值:react-use-state / react-use-reducer
- 需要和外部系统同步:react-use-effect / react-use-layout-effect
- 只想保存一个不触发渲染的值:react-use-ref
- 需要跨层传值:react-use-context
- 有昂贵计算或需要稳定对象引用:react-use-memo
- 需要稳定函数引用:react-use-callback
- 想复用一段“状态 + 副作用 + 行为”:react-custom-hooks
最容易误用的 Hook
最容易被滥用的是 useEffect。很多本该写在渲染逻辑或事件处理里的代码,被误塞进了副作用里。要单独理解时,优先看 react-use-effect。
最短记忆方式
- Hooks 不是“函数组件的补丁”
- Hooks 是函数组件获取 React 能力的统一接口
- 真正会用 Hooks 的标志,是能区分状态、引用、副作用和性能优化各自的边界
面试要点
来自 class-vs-function-component-interview-question 的面试视角整理。
一句话回答
我不太会把”学底层”理解成只刷源码。我更倾向于从实际问题出发反推原理:先在项目里遇到真实问题,再定位到对应抽象层,最后回到源码、文档或实验验证。
面试回答主线
学习方法论
- 先遇到问题:不做无目标的源码阅读,而是先在实际开发中碰到问题
- 定位抽象层:判断问题出在哪一层(框架、运行时、网络、浏览器)
- 回到源码和文档:带着问题去看实现,理解设计意图
- 实验验证:写小 demo 验证理解,而不是只看不练
实际例子
鉴权链路:
- 问题:token 刷新时并发请求怎么处理
- 定位:HTTP client 层的请求队列和状态管理
- 学习:看了 axios 拦截器机制和请求队列的实现
- 收获:理解了为什么要在刷新时阻塞其他请求
实时通信:
- 问题:SSE 流式响应被代理压缩破坏
- 定位:Vite 代理层和中间件行为
- 学习:看了 HTTP 压缩和 SSE 协议的规范
- 收获:理解了为什么 SSE 路径要关闭压缩
框架响应式:
- 问题:Vue 3 和 React 的更新机制差异
- 定位:响应式系统和调度器
- 学习:对比了 Proxy 和 VDOM diff 的设计
- 收获:理解了两种模型各自的优劣和适用场景
为什么这样学
- 有上下文:带着问题学,理解更深
- 能验证:可以直接在项目里试
- 不容易忘:和实际经验绑定
- 能迁移:学到的不是某个框架的 API,而是设计思路
常见误区
不要只说”我看源码”
看源码不是目的,理解设计意图才是。要说清楚”看了什么、为什么看、学到了什么”。
不要只背概念
比如知道 React 有 Fiber,但说不清楚它解决什么问题、怎么调度,这种理解不够。
不要脱离项目
纯理论学习容易遗忘,最好每个知识点都能和项目里的实际场景对应上。
最短记忆方式
学习底层 = 遇到问题 → 定位层次 → 看源码文档 → 实验验证 → 沉淀判断
一句话回答
class component 基于 OOP 模型,生命周期拆分明确但复杂逻辑容易分散;function component 结合 Hook 后,状态逻辑更贴近组合式思维,复用更自然。新项目优先函数组件,老项目不盲目迁移。
核心区别
编程模型
- class component:面向对象,继承
React.Component,状态挂在this.state,更新靠this.setState。 - function component:函数式,每次渲染是一次独立调用,状态通过 Hook 管理。
生命周期 vs 副作用
- class component:生命周期分得很细(
componentDidMount、componentDidUpdate、componentWillUnmount),但相关逻辑会被拆分到不同生命周期,不相关的逻辑又可能堆在同一个生命周期里。 - function component:
useEffect按”依赖什么”组织,而不是按”发生在哪个阶段”组织,相关逻辑更容易聚合在一起。更细的对应关系见 React 生命周期与 Hooks。
逻辑复用
- class component:主要靠 HOC 和 render props,容易出现 wrapper hell 和 props 命名冲突。
- function component:自定义 Hook 让有状态逻辑可以直接抽成函数,调用方按需组合,没有嵌套层级问题。
this 问题
- class component:需要处理
this绑定,方法要么在构造函数里 bind,要么用箭头函数。 - function component:没有
this,闭包捕获每次渲染的独立值,心智模型更一致。
代码对照:一个典型的订阅场景
下面假设有一个 createChatConnection(roomId),它会返回一个带有 connect、disconnect 和 onMessage 的连接对象。
class component
import React from 'react'
type ChatConnection = {
connect: () => void
disconnect: () => void
onMessage: (handler: (message: string) => void) => void
}
declare function createChatConnection(roomId: string): ChatConnection
type ChatRoomProps = {
roomId: string
}
type ChatRoomState = {
messages: string[]
}
class ChatRoom extends React.Component<ChatRoomProps, ChatRoomState> {
state: ChatRoomState = {
messages: []
}
connection: ChatConnection | null = null
componentDidMount() {
this.subscribe(this.props.roomId)
}
componentDidUpdate(prevProps: ChatRoomProps) {
if (prevProps.roomId !== this.props.roomId) {
this.unsubscribe()
this.subscribe(this.props.roomId)
}
}
componentWillUnmount() {
this.unsubscribe()
}
subscribe(roomId: string) {
const connection = createChatConnection(roomId)
connection.connect()
connection.onMessage((message) => {
this.setState((prevState) => ({
messages: [...prevState.messages, message]
}))
})
this.connection = connection
}
unsubscribe() {
this.connection?.disconnect()
this.connection = null
}
render() {
return (
<section>
<h3>Room: {this.props.roomId}</h3>
<ul>
{this.state.messages.map((message, index) => (
<li key={`${message}-${index}`}>{message}</li>
))}
</ul>
</section>
)
}
}
function component
import { useEffect, useState } from 'react'
type ChatConnection = {
connect: () => void
disconnect: () => void
onMessage: (handler: (message: string) => void) => void
}
declare function createChatConnection(roomId: string): ChatConnection
type ChatRoomProps = {
roomId: string
}
function ChatRoom({ roomId }: ChatRoomProps) {
const [messages, setMessages] = useState<string[]>([])
useEffect(() => {
const connection = createChatConnection(roomId)
connection.connect()
connection.onMessage((message) => {
setMessages((prevMessages) => [...prevMessages, message])
})
return () => {
connection.disconnect()
}
}, [roomId])
return (
<section>
<h3>Room: {roomId}</h3>
<ul>
{messages.map((message, index) => (
<li key={`${message}-${index}`}>{message}</li>
))}
</ul>
</section>
)
}
这组代码最值得看的是:class 组件把“订阅、切换、清理”拆到了不同生命周期里;函数组件把同一件事收进了一个 useEffect,依赖变化和清理逻辑放在一起。
面试回答主线
如果新项目,你会怎么选
优先函数组件。因为 Hook 生态是 React 当前和未来的主线,自定义 Hook 的逻辑复用、组合能力和可测试性都更好。
如果老项目 class 很稳定,要不要迁移
不建议为了迁移而迁移。class 组件只要行为正确、测试覆盖充分,继续维护就好。真正需要做的是在新模块和新需求上统一用函数组件。
函数组件有没有缺点
有。Hook 的依赖数组如果写错,容易出现 stale closure 和无限循环;闭包陷阱对新手不友好;性能优化需要理解 useMemo、useCallback 和 React.memo 的配合,否则容易过度优化或优化不足。
最短记忆方式
- class = 生命周期细、逻辑分散、
this麻烦、HOC 嵌套深 - function = Hook 组合、逻辑聚合、无
this、自定义 Hook 复用自然 - 结论 = 新项目优先 function,老项目不强行迁移