React Hooks

React Hooks 的整体分工、使用边界与常见模式总览。

#tech / dev / frame #resource / react #type / synthesis #status / growing

[!info] related notes

React Hooks

Hooks 让函数组件拥有状态、上下文、副作用、引用和值缓存等能力。它们不是一组零散 API,而是 React 组件能力的统一入口。

Hooks 也是 React 对组件逻辑组织方式的一次升级:它让函数组件拥有“状态、生命周期、副作用、上下文、性能缓存、可复用逻辑”这些能力。

为什么 Hooks 重要

  • 让函数组件取代大多数类组件场景
  • 让状态逻辑和副作用逻辑可以抽成自定义 Hook 复用
  • 让组件更容易按功能组织,而不是按生命周期切碎

Hooks 到底解决了什么

在 Hooks 之前:

  • 同一件事常被拆到 componentDidMountcomponentDidUpdatecomponentWillUnmount
  • 复用逻辑经常要靠 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 分成四组理解

状态类

  • useState
  • useReducer

它们负责让组件记住会影响渲染的值。

进一步看:react-use-state, react-use-reducer

副作用类

  • useEffect
  • useLayoutEffect

它们负责和 React 外部系统同步,而不是单纯“渲染后顺手做点事”。

进一步看:react-use-effect, react-use-layout-effect

引用与上下文类

  • useRef
  • useContext

它们分别解决“不参与渲染的可变值”和“跨层共享值”的问题。

进一步看:react-use-ref, react-use-context

性能与体验类

  • useMemo
  • useCallback
  • useTransition
  • useDeferredValue

它们主要用于优化重计算、引用稳定性和交互优先级。

进一步看:react-use-memo, react-use-callback

最重要的两条规则

  1. 只在 React 函数组件或自定义 Hook 中调用 Hook
  2. 只在顶层调用,不要放进条件、循环或嵌套函数里

原因不是风格偏好,而是 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 结构。

常见例子有:useFetchuseToggleuseDebounceusePrevious

进一步看:react-custom-hooks

Hooks 学习里真正难的点

真正难的不是记住 API 名字,而是搞懂:

  • 为什么 Hook 只能顶层调用
  • 为什么每次渲染都会形成新闭包
  • 为什么 useEffect 不是“渲染后随便写逻辑”
  • 为什么依赖数组必须真实反映依赖
  • 为什么 useMemo / useCallback 不是默认必加

一套选择 Hook 的思维框架

  1. 需要让 UI 记住某个值:react-use-state / react-use-reducer
  2. 需要和外部系统同步:react-use-effect / react-use-layout-effect
  3. 只想保存一个不触发渲染的值:react-use-ref
  4. 需要跨层传值:react-use-context
  5. 有昂贵计算或需要稳定对象引用:react-use-memo
  6. 需要稳定函数引用:react-use-callback
  7. 想复用一段“状态 + 副作用 + 行为”:react-custom-hooks

最容易误用的 Hook

最容易被滥用的是 useEffect。很多本该写在渲染逻辑或事件处理里的代码,被误塞进了副作用里。要单独理解时,优先看 react-use-effect

最短记忆方式

  • Hooks 不是“函数组件的补丁”
  • Hooks 是函数组件获取 React 能力的统一接口
  • 真正会用 Hooks 的标志,是能区分状态、引用、副作用和性能优化各自的边界

面试要点

来自 class-vs-function-component-interview-question 的面试视角整理。

补充来自 how-to-learn-underlying-principles-interview-question

一句话回答

我不太会把”学底层”理解成只刷源码。我更倾向于从实际问题出发反推原理:先在项目里遇到真实问题,再定位到对应抽象层,最后回到源码、文档或实验验证。

面试回答主线

学习方法论

  1. 先遇到问题:不做无目标的源码阅读,而是先在实际开发中碰到问题
  2. 定位抽象层:判断问题出在哪一层(框架、运行时、网络、浏览器)
  3. 回到源码和文档:带着问题去看实现,理解设计意图
  4. 实验验证:写小 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:生命周期分得很细(componentDidMountcomponentDidUpdatecomponentWillUnmount),但相关逻辑会被拆分到不同生命周期,不相关的逻辑又可能堆在同一个生命周期里。
  • function componentuseEffect 按”依赖什么”组织,而不是按”发生在哪个阶段”组织,相关逻辑更容易聚合在一起。更细的对应关系见 React 生命周期与 Hooks

逻辑复用

  • class component:主要靠 HOC 和 render props,容易出现 wrapper hell 和 props 命名冲突。
  • function component:自定义 Hook 让有状态逻辑可以直接抽成函数,调用方按需组合,没有嵌套层级问题。

this 问题

  • class component:需要处理 this 绑定,方法要么在构造函数里 bind,要么用箭头函数。
  • function component:没有 this,闭包捕获每次渲染的独立值,心智模型更一致。

代码对照:一个典型的订阅场景

下面假设有一个 createChatConnection(roomId),它会返回一个带有 connectdisconnectonMessage 的连接对象。

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 和无限循环;闭包陷阱对新手不友好;性能优化需要理解 useMemouseCallbackReact.memo 的配合,否则容易过度优化或优化不足。

最短记忆方式

  • class = 生命周期细、逻辑分散、this 麻烦、HOC 嵌套深
  • function = Hook 组合、逻辑聚合、无 this、自定义 Hook 复用自然
  • 结论 = 新项目优先 function,老项目不强行迁移
创建于 2025/1/1 更新于 2026/5/27