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 项目就会稳很多。