React 社区关于状态管理的讨论,很多时候像宗教战争。
一拨人会说 Context 已经够用了,没必要引 store;一拨人觉得 Zustand 足够轻,正好卡在“好用但不重”的区间;还有一拨人会强调 Redux 才适合真正复杂的系统。
这些观点都不算错,但我越来越觉得,这类讨论经常从一开始就问错了问题。
真正决定状态管理方案的,不是某个库的口碑,也不是团队最近流行什么,而是这一句:
这个状态,到底属于谁?
它属于当前组件,还是整个页面?
它应该跟着路由走,还是跟着用户会话走?
它是短暂的 UI 临时态,还是需要长期追踪的业务状态?
它是客户端主动创造的,还是服务端返回、会失效、会重刷的数据?
如果这些问题不先分清楚,你换多少工具,最后都还是会乱。
很多项目的问题,不是用了错的库,而是把错的东西放进了全局
我见过太多 React 项目,一开始只是想“统一管理状态”,结果越做越像把一切都往全局 store 里塞。
输入框临时值进全局,弹窗开关进全局,当前 hover 状态进全局,接口返回列表也复制一份进全局。最后整个项目看起来是“有状态管理体系”,实际上只是把局部问题集中存放了。
这类项目的共同特征是:大家讨论库讨论得很热闹,但很少认真问“这个状态到底有没有必要脱离当前组件生存”。
所以我现在做状态设计时,第一步永远不是选 Context、Zustand 还是 Redux,而是先粗暴地给状态分类。
通常我会这样分:
| 状态类型 | 例子 | 更合适的位置 |
|---|---|---|
| 局部 UI 状态 | 输入框、弹窗、hover、tab | 组件内部 |
| 页面状态 | 分页、筛选、排序、当前视图模式 | 页面组件或 URL |
| 全局上下文 | 主题、语言、当前用户基础信息 | Context |
| 远程服务端状态 | 列表数据、详情数据、缓存、失效 | 请求缓存层 |
| 复杂客户端业务状态 | 多步骤编辑、工作流、协作中间态 | Zustand / Redux |
你会发现,大量所谓“状态管理问题”,其实在这张表里就已经被消解了。
Context 最适合做的,是传递上下文,而不是承担状态系统
Context 在 React 里常常被高估,也常常被低估。
被高估,是因为很多人喜欢把它当轻量版全局 store 来用;被低估,是因为也有人因此得出结论说它“只能做一点小东西”。
我自己的理解是:Context 本身没有问题,问题在于它的设计目的就不是为了替代完整状态管理。
它最适合承载的是那种更新频率不高,但作用范围确实跨树的东西,比如:
- 当前语言
- 主题
- 登录用户基础信息
- 权限上下文
- 全局配置
- 某种注入式服务对象
例如一个语言 provider:
const LangContext = createContext<LangContextValue | null>(null)
export function LangProvider({ children }: { children: React.ReactNode }) {
const [lang, setLang] = useState<'cn' | 'en'>('cn')
return (
<LangContext.Provider value={{ lang, setLang }}>
{children}
</LangContext.Provider>
)
}
这类场景用 Context 很合理,因为它表达的就是“我需要把一个跨层级的上下文传下去”。
问题出在另一种情况:把高频变化、结构复杂、带很多 action 和副作用的东西也塞进 Context。
为什么这会变难用?因为 Context 的更新模型决定了,Provider value 一旦变化,消费它的组件就都会有感知。你当然可以继续做 memo、拆 Provider、拆 value,但一旦项目走到这里,通常就已经说明它不再适合只靠 Context 承担了。
所以我对 Context 的一句话总结是:它很适合传递环境,不适合充当复杂状态机。
页面状态很多时候不应该进 store,而应该进 URL
这是我这些年越来越强的一个判断。
很多项目把筛选条件、分页、tab、搜索关键词全都放在 store 里,技术上当然能跑,但你很快会失去一件很重要的东西:状态的可分享性和可恢复性。
比如博客列表页,如果当前分类、标签和页码都只存在内存里,那么:
- 刷新页面丢状态
- 复制链接无法复现当前视图
- 前进后退和筛选行为不自然
- SSR / SEO 场景下状态表达也很别扭
而如果它们进入 URL:
/blog?category=tech&tag=react&page=2
很多问题自然就被解决了。
所以我现在看到“页面状态要不要进 Zustand / Redux”时,脑子里会先多问一句:它其实是不是应该进 URL?
这一步非常值。因为一旦状态适合 URL,却被你放进 store,后面会不断有人试图“再把它同步到地址栏”,而那时候复杂度往往已经翻倍。
Zustand 为什么讨喜:因为它刚好卡在“够用又不烦”的区间
Zustand 这些年受欢迎,不是偶然。
它最打动人的地方其实很简单:相当一部分 React 项目,状态确实已经超出了 Context 的舒服区间,但又远没到需要一整套 Redux 约束体系的地步。Zustand 刚好填上了这个空档。
它的模型很直接:一个 store,一组状态,一组 action,需要什么取什么。
import { create } from 'zustand'
type FilterState = {
keyword: string
tags: string[]
setKeyword: (keyword: string) => void
toggleTag: (tag: string) => void
reset: () => void
}
export const useFilterStore = create<FilterState>((set) => ({
keyword: '',
tags: [],
setKeyword: (keyword) => set({ keyword }),
toggleTag: (tag) =>
set((state) => ({
tags: state.tags.includes(tag)
? state.tags.filter((item) => item !== tag)
: [...state.tags, tag],
})),
reset: () => set({ keyword: '', tags: [] }),
}))
你可以按 selector 订阅局部状态:
const keyword = useFilterStore((state) => state.keyword)
const setKeyword = useFilterStore((state) => state.setKeyword)
这个体验为什么舒服?因为它很少打断你。
它不会逼你先定义一大堆 action type、reducer、slice、middleware 才能开始工作。对很多中小复杂度的客户端状态来说,这已经足够。
比如这些场景我就很愿意用 Zustand:
- 多步骤表单的编辑草稿
- 复杂筛选器状态
- 编辑器或画布类交互状态
- 跨多个组件共享的临时业务上下文
- 某种需要集中 action 的页面级交互流
它特别适合那种“状态确实共享、确实复杂了一些,但你还不想为了它搭一套制度”的阶段。
但 Zustand 也最容易被用成“顺手全塞进去”
恰恰因为它太顺手,所以它也很容易成为新的杂物间。
我见过一些项目,最后演变成这样:只要某个状态跨了两个组件,就放 Zustand;只要某个数据以后可能复用,就放 Zustand;只要不想 prop drilling,就放 Zustand。最后 store 里既有用户信息,又有筛选条件,又有接口缓存,又有临时弹窗状态,还混着一堆半同步半异步 action。
这不是 Zustand 的问题,是人性的问题。
所以我给自己设的边界很明确:Zustand 更适合客户端状态,不适合替代服务端状态管理。
接口返回的数据如果本身有请求、缓存、失效、后台刷新这些生命周期,我更愿意把它放在专门的数据请求层,而不是复制一份进 Zustand。
为什么?因为服务端状态和客户端状态天然不是一回事。
客户端状态通常是“我当前怎么想”;服务端状态更多是“远端现在给了我什么,它什么时候会变、什么时候要重新拿”。这两类状态混进同一套 store,经常会造成后面刷新语义和一致性都变得模糊。
所以 Zustand 虽然很灵活,但它真正好用的前提,是你知道哪些东西不该放进去。
Redux 到今天依然有价值,但它的价值不是“更专业”,而是“更可控”
Redux 这些年经常被描述成“太重了”“样板代码太多”“小项目没必要”。这些评价并不冤枉,但我不认为它因此就过时了。
Redux 真正擅长的从来不是让你写得更快,而是让复杂系统中的状态变化变得更显式、更可追踪。
这一点在小项目里感受不明显,但只要系统复杂度上来,它就会开始变得重要。
例如:
dispatch(orderSubmitted({ orderId }))
dispatch(paymentFailed({ reason }))
dispatch(userPermissionChanged({ role }))
这些 action 不只是触发更新,它们本身还是业务事件。
当系统涉及订单流、审批流、权限流、协作状态、离线同步或者跨团队协作时,Redux 的强约束反而是优点。因为它让很多本来会隐式发生的状态变化,必须以显式事件形式出现。
对大型项目来说,这种“啰嗦”是有价值的。它会降低“谁在什么时候改了这个状态”的不透明感。
我会考虑 Redux 的场景通常有这些特征:
- 团队多人协作
- 状态变化路径复杂
- 调试和追踪成本高
- 需要明确 action 语义
- 业务状态远多于纯 UI 状态
在这种项目里,Redux 的问题不是太重,而是你能不能真正把它用到该用的地方。
Redux 最大的成本,不在写代码,而在你得接受一整套纪律
很多人说 Redux 麻烦,通常只是在说它代码多。其实代码多只是表面成本,真正的成本是:你得接受一种更制度化的开发方式。
状态要明确归属,更新要走 action,业务事件最好能命名清楚,中间件和副作用要有固定位置,数据流不应该太随意。
这套纪律对大型项目很有帮助,但对简单场景来说就可能显得过重。一个只在当前页面使用的弹窗开关、一个输入框临时值、一个局部 hover,如果都用 Redux 管,你只是把心智负担抬高了。
所以 Redux 并不是“越大型项目越正确”,而是“只有当状态复杂度真的需要纪律时,它的纪律才值钱”。
我实际做判断时,顺序比工具名字重要
如果让我把自己的判断流程说得尽量简单,它大概是这样的。
先问:能不能留在组件内部
如果这个状态只服务当前组件,那默认就先用组件自己的状态。
不要因为未来“也许会复用”就提前全局化。很多状态一旦提升到全局,后面反而更难收回来。
再问:它是不是页面状态,应该进 URL
筛选、搜索、分页、排序、视图模式,这类状态先想 URL,而不是先想 store。
再问:它是不是低频全局上下文
如果是语言、主题、当前用户基础信息、配置项,Context 通常够了。
再问:它是不是中等复杂度的客户端共享状态
如果多个组件共享,更新频率不低,又带一点业务动作,那么 Zustand 大概率是舒服区间。
最后再问:它是不是一个需要强约束和可追踪性的复杂系统
如果答案是是,那才认真考虑 Redux。
你会发现,这个过程里“用哪个库”其实是最后一步。前面几步才决定了你到底有没有在处理一个真正的全局状态问题。
一个项目里同时存在多种方案,并不代表架构混乱
这点我很想单独说一下。
很多团队会追求“全项目统一一个状态管理方案”,仿佛只有这样架构才算整洁。我不完全认同。
真正健康的项目,往往本来就会分层:
- 弹窗开关在组件内部
- 列表筛选在 URL
- 主题语言在 Context
- 编辑草稿在 Zustand
- 某些特别复杂的业务流在 Redux
这不是混乱,这是尊重状态本身的性质。
真正的混乱恰恰是另一种:为了“统一”,把本来就不是一类的状态全塞进同一个工具里。那种统一只是表面统一,内部复杂度反而更糟。
最后
React 状态管理最有误导性的地方,就是它总让人觉得自己在选库。
其实你真正选的是另一件事:状态的生命周期、作用范围和变化方式。
如果这个判断做对了,Context、Zustand、Redux 都能各自待在舒服的位置;如果这个判断做错了,再好的库也救不了一个把所有东西都往全局堆的项目。
所以我现在对状态管理的态度很简单:
- 能局部就局部
- 适合 URL 的别硬塞 store
- 低频上下文用 Context
- 中等复杂客户端状态用 Zustand
- 真正复杂、需要纪律和追踪的系统再用 Redux
工具不是答案,状态本身才是。