返回专题首页

React 专题

React 状态分层:本地状态、Context、URL 状态与服务端状态

React 状态管理最容易被简化成“选哪个库”,但真正应该先回答的,是“状态本身有几类”。按钮 loading、表单输入、主题、登录态、列表缓存、分页参数,这些状态的生命周期、共享范围和协作方式完全不同。如果不先分层,后面无论用 Context、Redux 还是 Zustand,

React 专题第 21 篇 / 26 篇4 分钟

React 状态管理最容易被简化成“选哪个库”,但真正应该先回答的,是“状态本身有几类”。按钮 loading、表单输入、主题、登录态、列表缓存、分页参数,这些状态的生命周期、共享范围和协作方式完全不同。如果不先分层,后面无论用 Context、Redux 还是 Zustand,都很容易越来越重。

为什么状态分层是 React 项目稳定性的前提?

因为真实项目里,状态天然就不是单一类型。你通常会同时遇到:

  • 只服务当前组件的本地状态;
  • 需要在一棵子树内共享的上下文状态;
  • 本来就适合进 URL 的导航状态;
  • 带缓存、失效和重试问题的服务端状态。

只要这些边界没分清,常见结果就是:

  • 本地状态被抬得过高;
  • Context 承接了太多高频业务状态;
  • 适合进 URL 的状态只放内存;
  • 服务端数据和客户端状态全混在一起。

本地状态为什么应该尽量留在本地?

本地状态通常具备几个特征:

  • 生命周期短;
  • 只影响当前组件或局部子树;
  • 页面离开后往往不必保留;
  • 和具体交互强绑定。

典型如:

  • 弹窗开关;
  • 当前输入值;
  • 局部 loading;
  • 某个 tab 内部切换;
  • 某一行是否展开。

如果这类状态一上来就被抬到共享层,短期看似统一,长期几乎一定会增加维护成本。

Context 状态最适合承接什么?

Context 最适合少量稳定共享的上下文,比如:

  • 主题;
  • 语言环境;
  • 认证信息;
  • 某个组件族的局部上下文;
  • 低频共享能力。

它最大的价值是避免层层透传。但如果把高频变化的业务状态、复杂列表条件、局部交互全压进去,Context 很快就会变成一个不太可控的共享容器。

URL 状态为什么值得单独拿出来看?

很多 React 页面里的:

  • 页码;
  • 筛选条件;
  • 排序方式;
  • tab;
  • 当前详情 ID;
  • 某些搜索关键字

本来就适合被地址表达。这样做的好处包括:

  • 刷新不丢上下文;
  • 链接可以分享;
  • 前进后退更自然;
  • 页面恢复能力更强。

如果这些状态只保存在内存里,用户体验会明显变差,尤其是在中后台和内容型页面里。

服务端状态为什么不能简单硬塞进客户端 store?

因为它和普通客户端状态根本不是一类问题。服务端状态天然还伴随:

  • 请求生命周期;
  • 缓存与失效;
  • 刷新与重试;
  • 乐观更新;
  • 列表与详情一致性;
  • 错误恢复。

这说明它本质上是“远程数据协作问题”,而不是单纯“存哪里”的问题。如果和本地 UI 状态混在一起,后面通常会越来越难治理。

一个更实用的判断顺序

下次遇到一份新状态时,可以先问:

1. 这份状态是否只服务当前组件?2. 是否只是某棵局部子树需要共享?3. 它是否值得进 URL?4. 它是不是来自服务端,是否有缓存与失效问题?5. 离开当前页面后,这份状态还需不需要保留?

只要这五个问题先答清楚,后面的工具选择通常都会顺很多。

最常见的几个误区

1. 所有状态都试图统一进一个共享层

最后会明显失去边界。

2. Context 承接太多高频业务状态

会让共享层越来越重。

3. 适合进 URL 的状态只放内存

刷新和分享体验会变差。

4. 服务端数据和本地 UI 状态混写

长期很难保持清晰。

总结

React 状态分层真正要建立的,不是某套库的熟练度,而是“状态到底属于哪一层”的判断力。本地状态留在本地,Context 承接少量稳定上下文,URL 状态负责页面可恢复性,服务端状态则围绕远程数据协作来建模。只要这四层分清楚,React 项目就会稳很多。