React Hooks 介绍与实践要点

React Hooks 介绍与实践要点

本文面向日常业务开发,聚焦 核心 Hooks典型场景 Hooks ,并补充常见的数据请求方案(SWR / React Query)、浏览器渲染过程与 useEffect / useLayoutEffect 的关系,最后简要介绍 Redux 与 React Navigation 常用 Hooks。


核心 Hooks

useState:组件内状态

ts 复制代码
const [state, setState] = useState(initialState);
  • 更新判定 :React 使用 Object.is 比较新旧 state,只有"不相等"才会触发更新(这也是为什么"设置相同值"通常不会 re-render)。
  • Object.is 的两个关键差异点 (相对 ===):
    • NaNObject.is(NaN, NaN)true,而 NaN === NaNfalse
    • +0-0Object.is(+0, -0)false,而 +0 === -0true

实践建议:

  • 状态是对象/数组时 ,务必用"新引用"更新(不可原地修改),否则 Object.is 会认为没变(同一引用)而不触发更新。

useEffect:提交渲染后异步执行副作用

useEffect 会在 React 完成渲染并提交 DOM 更新之后 异步执行,通常不会阻塞浏览器绘制(paint),适合:

  • 数据请求、订阅、日志/埋点(不依赖布局测量)、与非阻塞型副作用

依赖数组要点:

  • 依赖缺失可能导致使用到"旧闭包值"(stale closure)
  • 依赖过多容易引发重复请求或循环触发,需要拆分 effect 或改用更高层的数据方案(SWR / React Query)

useRef:引用不参与渲染的可变值 / DOM 引用

  • 典型用途
    • 保存不会引起 re-render 的值(如计时器 id、上一次值、可变标记)
    • 获取 DOM/原生组件实例(需要时进行测量、聚焦等)
  • 关于传递 ref
    • React 18:子组件要"透传 ref"通常需要 forwardRef
    • React 19:ref 支持作为普通 prop 传递(生态在逐步适配),但仍需关注组件实现是否接收并正确挂载到目标节点

useImperativeHandle:受控暴露子组件能力(配合 ref)

当你希望父组件通过 ref 调用子组件能力(如 focus()scrollTo()),但又不想把子组件内部实现(真实 DOM/实例)完全暴露出去时,可以用 useImperativeHandle 做"白名单式暴露":

  • 对外只暴露必要方法/属性
  • 对内仍保留组件封装边界

通常与 forwardRef(React 18 常见用法)或 React 19 的 ref 语义配合。


useReducer:更可控的状态演进(适合复杂状态)

当 state 结构复杂、更新逻辑分支多、或者需要更强可测试性时,useReducer 往往比多个 useState 更清晰:

  • 把更新逻辑集中到 reducer,避免散落在各个事件处理里
  • 更利于维护与单测(输入 action,输出新 state)

useContext:跨层级共享状态与依赖注入

useContext 用于订阅 context 值,适合:

  • 主题、语言、用户信息、权限、配置、依赖注入(API 客户端等)

注意点:

  • Context 值变化会触发所有消费组件更新 。若 context 对象频繁变化,可考虑:
    • 拆分 context(按关注点拆)
    • 提供稳定引用(useMemo 包裹 value)
    • 引入 selector 模式或外部状态库

数据请求:useEffect vs SWR / React Query

直接用 useEffect 的局限

useEffect 做请求通常意味着你需要自行处理:

  • loading / error / data 的状态机
  • 缓存(组件卸载再挂载通常会重新请求)
  • 自动刷新 / 重试 / 失焦恢复 等策略
  • 复杂依赖管理:依赖多时容易遗漏依赖数组、或触发循环请求

SWR / React Query(TanStack Query)

它们把"请求"升级为"客户端数据层",提供:

  • 缓存与去重:相同 key 的请求能共享结果
  • 自动重试、失焦恢复、刷新策略
  • 更好的并发与竞态控制(避免过期请求覆盖新结果)

React Query 相对 SWR 往往更"全能":

  • 更成熟的 分页 / 无限滚动 能力
  • 更复杂的依赖联动与查询控制(例如 enabled、query key 组合)

依赖数据联动(Dependent Queries):先有 A 再请求 B

很多业务请求不是"并行独立"的,而是 后一个请求依赖前一个请求的结果 (例如:先拿用户信息,再用 userId 拉订单)。如果用 useEffect 手写,常见问题是:

  • 依赖判断散落在多个 effect 里,容易漏依赖或出现重复请求
  • 前后请求的 loading/error 状态难以组合
  • 竞态条件(旧请求返回覆盖新请求)需要额外处理

React Query 通常用 queryKey + enabled 表达"依赖准备好才发请求":

ts 复制代码
const { data: users } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
});

const userId = users?.[0]?.id;

const { data: orders } = useQuery({
  queryKey: ['orders', userId],
  queryFn: () => fetchOrders(userId!),
  enabled: !!userId, // 依赖就绪才触发
});

要点:

  • queryKey 必须包含依赖参数 (如 userId),否则缓存命中与更新会错乱
  • enabled 负责"何时允许请求",避免依赖未就绪时请求报错或无意义请求

分页 vs 无限滚动(为什么需要 useInfiniteQuery)

  • 分页 :按页码分块拉取(如 page=1&limit=20),适合明确翻页的列表
  • 无限滚动:滚动到临界点加载下一页,更贴近信息流体验

在手写方案里,你要维护 page、合并列表、处理 loading/error、判断是否还有下一页;而 React Query 的 useInfiniteQuery 会把这些常用模式封装好(页缓存、hasNextPage、合并等)。


性能与体验优化 Hooks

useMemo:缓存"计算结果"

适用于 昂贵计算 或需要 稳定引用 的派生值,避免每次渲染都重复计算、或导致子组件误触发更新。

要点:

  • useMemo 不是"免费"的;依赖很少变化、计算很重时收益更大
  • 不要把它当"到处都要加"的默认优化

useCallback:缓存"函数引用"

用于将事件处理函数/回调稳定化,尤其是:

  • 作为 props 传给 memo 子组件
  • 作为依赖传给其它 hooks(如 useEffect

useTransition:标记低优先级更新(可被打断)

用于把非紧急的更新放入 transition,让输入、点击等高优先级交互更流畅:

  • 典型场景:输入框过滤大列表、切换复杂视图等
  • isPending 可用于展示加载态/骨架屏等

useDeferredValue:为某个值提供"延迟版本"

适合 由 props/state 变化引起的慢渲染 ,但更新发起点不在当前组件、或不方便用 startTransition 包裹的情况,一般用于父组件传递props值变化引起子组件重渲染的情况。

对比:

  • useTransition:关注"这次更新操作"应低优先级、可中断
  • useDeferredValue:关注"使用这个值的渲染"可以晚一点发生

useSyncExternalStore:订阅外部 Store(并发渲染一致性 + 可控性能)

useSyncExternalStore 面向"外部状态源"(Redux、Zustand、自建 store、EventEmitter 等)。它的核心价值不是语法糖,而是让 React 在 并发渲染(Concurrent Rendering) 下读取外部状态时保持一致性(避免 tearing),并把订阅/快照的时机与 React 渲染流程对齐。

为什么不用 useEffect + subscribe + setState

useEffect 的订阅发生在 commit 之后:组件首屏 render/commit 时可能还没订阅成功;如果外部 store 在这段窗口内变化,组件会短暂展示旧值,甚至在并发渲染下出现"同一帧内不同组件读到不同版本"的撕裂(tearing)。

useSyncExternalStore 的模型是:

  • render 阶段读取 snapshot :通过 getSnapshot() 获取当前 store 的一致性快照
  • React 负责订阅时机 :通过 subscribe() 在合适时机建立订阅,并在 store 变更时触发重新读取 snapshot
  • 以 snapshot 作为真相来源:组件渲染只依赖 snapshot,而不是"effect 里手动 setState"
API 形状与关键约束
ts 复制代码
const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
  • subscribe(onStoreChange) => unsubscribe,store 变化时调用 onStoreChange
  • getSnapshot :返回当前快照值;必须在"未变化"时返回同一个引用 (或至少让 Object.is 判定相等),否则会造成无意义重渲染
  • getServerSnapshot(SSR 可选):服务端渲染时使用的快照读取方式,保证 hydration 一致
与 Redux / Zustand 的关系(你可能"间接"在用)
  • React-Redux v8 内部用 useSyncExternalStore 实现订阅一致性,所以 useSelector 在并发渲染下更安全。
  • Zustand 等库也会用同类机制(或提供 selector 版本)来避免"读到不一致状态"。

实践建议:

  • 能用成熟库就别手写:外部 store 的边界条件很多(并发一致性、选择器、性能、订阅风暴)。
  • 需要 selector 时:优先使用库自带 selector,或使用官方 shim 的 selector 版本思路(避免每次 snapshot 都导致大对象变化)。

useOptimistic(React 19+):乐观更新

在异步操作真正完成前,先在 UI 上展示"假定成功"的状态;当操作失败时,React 会丢弃对应 transition 的更新,使回滚更简单,这个天然需要配合useTransition使用,因为useTransition当获取数据失败之后可以自动回滚,取消渲染,不需要手动callback更新旧值。

适用场景:

  • 成功率高、回滚代价小(例如点赞、收藏、轻量新增)

浏览器渲染过程与 useLayoutEffect / useEffect

理解一个关键时序有助于正确选择副作用 hooks:

  • render:计算 Virtual DOM(Fiber)
  • commit:把变更应用到真实 DOM
  • paint:浏览器把像素绘制到屏幕

useLayoutEffect:在 paint 之前执行(会阻塞绘制)

特点:

  • DOM 已更新但屏幕尚未绘制
  • 适合:必须读取布局(测量尺寸/位置)并立刻同步写回,避免闪动(layout shift)
  • 风险:逻辑重会阻塞 paint,造成卡顿

useEffect:在 paint 之后执行(通常不阻塞首屏)

特点:

  • 更适合非布局相关副作用(请求、订阅、日志、非关键 DOM 操作)

为什么动画常推荐 transform / opacity

浏览器渲染大致包含:

  • 解析 HTML 构建 DOM Tree
  • 解析 CSS 构建 CSSOM
  • 合并生成 Render Tree
  • layout(回流:计算几何信息)
  • paint(绘制)
  • composite(合成层,通常更快)

transform / opacity / will-change 更容易走合成路径,减少 layout/paint 压力,动画更顺滑。

示例(提示浏览器提前优化某些属性的变化):

css 复制代码
.box {
  will-change: transform;
}

.box:hover {
  transform: translateX(100px);
  transition: transform 0.3s;
}

浏览器解析 HTML 时遇到脚本,会影响"解析、下载、执行"的时机:

  • 普通脚本<script src="app.js"></script>
  • 解析 HTML 会被暂停,等待下载并执行脚本后再继续解析
  • defer<script src="app.js" defer></script>
  • 解析 HTML 与下载脚本并行
  • 等 DOM 构建完成后按顺序执行(更适合依赖 DOM 的业务脚本)
  • async<script src="analytics.js" async></script>
  • 解析 HTML 与下载脚本并行
  • 脚本下载完成立刻执行(执行时会中断 HTML 解析),执行顺序不可控
  • 适合不依赖 DOM/不依赖其它脚本的场景(如埋点)
  • module<script type="module" src="main.js"></script>
  • 类似 defer 的加载时机,但 支持 ESM 依赖解析(import/export)
  • 会根据依赖图决定执行顺序(不是简单按标签出现顺序)

特殊场景 Hooks(1--2 句话概括)

useId:生成稳定且避免冲突的 id

用于表单控件 label/input 关联等;SSR 场景下可以保证服务端与客户端生成一致。

useInsertionEffect:更早插入样式(CSS-in-JS 场景)

在 DOM 插入前运行,常用于运行时注入样式,时机早于 useLayoutEffect;一般业务组件很少直接使用。

useDebugValue:为自定义 Hook 提供 DevTools 展示信息

只影响开发者工具显示,不影响渲染逻辑,适合提升自定义 hooks 的可调试性。

useActionState(React 19+):面向 action 的状态管理

更适合与 action/表单提交一类交互配合,帮助在事件/动作驱动下获得更一致的状态流(常见于新形态表单与异步提交流程)。

useEffectEvent(React 新增能力):避免闭包陈旧值的事件回调

用于创建"不会因为渲染而重建、但能读取最新 state"的事件回调,减少把事件处理函数塞进依赖数组引发的复杂性。


Redux 常用 Hooks(React-Redux)

useSelector:从 store 读取并订阅片段状态

用于选择需要的 state 片段;建议搭配 selector 与 memo 化策略,避免不必要的重渲染。

useDispatch:派发 action

获取 dispatch 用于触发更新;在大型应用中常配合 thunk/saga 或 RTK Query 管理异步与数据请求。

useStore:获取 store 实例(少用)

一般业务很少需要直接操作 store 实例;更多用于框架层集成或极少数高级场景。


不同版本 API 略有差异,以下为最常见的概念性用法。

用于跳转、返回、设置标题等(如 navigate/goBack/setOptions),是函数组件里最常见的导航入口。

useRoute:获取当前路由信息

用于读取路由参数与当前页面的 route 元数据(如 params)。

useFocusEffect:页面聚焦时执行副作用

用于"进入页面/返回页面时刷新数据或注册监听"的场景,语义上比 useEffect 更贴合导航生命周期。

useIsFocused:判断页面是否聚焦

适合控制"仅在当前页面可见时运行"的逻辑,例如暂停轮询、暂停动画/视频等。


常见误区与最佳实践速记

  • 依赖数组不是可选项:缺依赖往往不是"优化",而是潜在 bug(陈旧值、错过更新)。
  • 不要过度 memouseMemo/useCallback 适用于瓶颈点;到处包裹会增加心智负担与维护成本。
  • 副作用分层:数据请求与缓存优先交给专用库(SWR/React Query),组件只负责展示与交互。
  • 选对 effect 时机 :布局测量/避免闪动用 useLayoutEffect,其他尽量用 useEffect
相关推荐
康一夏2 小时前
CSS盒模型(Box Model) 原理
前端·css
我是小疯子662 小时前
JavaScriptWebAPI核心操作全解析
前端
小二·2 小时前
Python Web 开发进阶实战:全链路测试体系 —— Pytest + Playwright + Vitest 构建高可靠交付流水线
前端·python·pytest
貂蝉空大2 小时前
vue-pdf-embed分页预览解决文字丢失问题
前端·vue.js·pdf
满天星辰2 小时前
Typescript的infer到底怎么使用?
前端·typescript
ss2732 小时前
RuoYi-App 本地启动教程
前端·javascript·vue.js
Jolyne_2 小时前
useRef存在的潜在性能问题
前端
炫饭第一名2 小时前
Lottie-web 源码解析(一):从 JSON Schema 认识 Lottie 动画的本质📒
前端·javascript·css
Zyx20072 小时前
防抖与节流:用闭包驯服高频事件的性能利器
react.js