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的两个关键差异点 (相对===):NaN:Object.is(NaN, NaN)为true,而NaN === NaN为false+0与-0:Object.is(+0, -0)为false,而+0 === -0为true
实践建议:
- 状态是对象/数组时 ,务必用"新引用"更新(不可原地修改),否则
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 传递(生态在逐步适配),但仍需关注组件实现是否接收并正确挂载到目标节点
- React 18:子组件要"透传 ref"通常需要
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 实例;更多用于框架层集成或极少数高级场景。
React Navigation 常用 Hooks(移动端导航)
不同版本 API 略有差异,以下为最常见的概念性用法。
useNavigation:获取 navigation 对象
用于跳转、返回、设置标题等(如 navigate/goBack/setOptions),是函数组件里最常见的导航入口。
useRoute:获取当前路由信息
用于读取路由参数与当前页面的 route 元数据(如 params)。
useFocusEffect:页面聚焦时执行副作用
用于"进入页面/返回页面时刷新数据或注册监听"的场景,语义上比 useEffect 更贴合导航生命周期。
useIsFocused:判断页面是否聚焦
适合控制"仅在当前页面可见时运行"的逻辑,例如暂停轮询、暂停动画/视频等。
常见误区与最佳实践速记
- 依赖数组不是可选项:缺依赖往往不是"优化",而是潜在 bug(陈旧值、错过更新)。
- 不要过度 memo :
useMemo/useCallback适用于瓶颈点;到处包裹会增加心智负担与维护成本。 - 副作用分层:数据请求与缓存优先交给专用库(SWR/React Query),组件只负责展示与交互。
- 选对 effect 时机 :布局测量/避免闪动用
useLayoutEffect,其他尽量用useEffect。