返回博客列表
技术文章React状态管理ContextZustandRedux前端架构

React 项目复杂状态管理:什么时候用 Context、Zustand、Redux

2026年05月🇨🇳 中文

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

工具不是答案,状态本身才是。