#Next.js#React

React 状态管理选型:从 useState 到 Zustand

状态管理是 React 永恒的话题,选择太多反而乱。本文讲清状态管理的层级(useState/useReducer/context/Zustand/Redux 各管哪层)、本地状态 vs 服务端状态(React Query/SWR)、什么场景该上全局库、什么时候 context 就够,帮你跳出"什么都上 Redux"的误区。

本文要点

  • 主题聚焦:Next.js、React
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年6月11日·更新于 2026年7月23日

React 状态管理选型:从 useState 到 Zustand

每接手一个 React 老项目,你几乎都能看到同样的一幕:根组件套着一个巨大的 Redux Store,里面塞着主题色、用户信息、表单临时输入、列表数据、分页页码……然后所有人都抱怨它太重、太绕、改一个字段要动五个文件。

这不是 Redux 的错,是选型的错。状态管理从来不是"选最强的那个",而是"在合适的层级选最轻的那个"。这篇文章把 React 状态管理这件事讲透:状态分几层、每一层该用什么、什么时候该上库、什么时候 context 就够,以及为什么服务端状态根本不该进 Redux。


一、状态管理不是越强越好,按场景选最轻的

一个常见误区是:"项目大了就要上 Redux。" 这句话在 2018 年是对的,今天基本是错的。

原因很简单——React 自从 Hooks 出来以后,自带的状态能力已经非常强了。useState、useReducer、useContext,这三板斧能解决 80% 的状态需求。再加上 React Query / SWR 接管服务端状态,真正需要 Redux / Zustand 这类全局库的场景,被进一步压缩到了很窄的一段。

选型的核心原则只有一句:用能解决问题的最轻方案。 多余的抽象就是未来的债。一个 useState 能搞定的东西,你非要塞进 Redux,得到的不是"可维护性",而是"改一行代码要跳五个文件"。

所以正确的提问不是"用什么状态管理库",而是"这个状态属于哪一层"。把层分对了,库的选择基本就自动出来了。


二、状态分层:把状态放到正确的层级

我个人把 React 应用里的状态分成四层,从轻到重:

层级归属典型工具例子
组件本地状态单个组件内部useState / useReducer输入框值、折叠展开、loading 标志
跨组件共享状态一棵子树内Context + useState主题、当前用户、i18n 语言
全局应用状态整个 AppZustand / Redux / Jotai购物车、多步表单聚合、复杂 UI 编排
服务端状态后端数据缓存React Query / SWR接口列表、详情、用户资料

这四层之间有一个非常重要的判断:先看状态属于哪一层,再决定工具。 很多团队的问题不是工具选错了,而是根本没分清"这是本地状态还是服务端状态",把接口返回的列表数据塞进了 Redux,然后手写 loading / error / refetch / 分页 / 失效,最后把自己写成了一个低配版的 React Query。

下面每一层我们展开说。


三、useState/useReducer:够用就别加库

这是 90% 状态的归宿。一个状态如果只有当前组件(或它的直接子组件通过回调)会读写,那就老老实实待在组件里。

tsx
function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
  const [query, setQuery] = useState('');
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onKeyDown={(e) => e.key === 'Enter' && onSearch(query)}
    />
  );
}

query 这种状态没有任何理由上 Context,更没有理由进 Redux。它只活在这一个组件里,组件卸载就销毁,干净利落。

当本地状态变复杂(多个字段联动、状态转换有规则)时,升级到 useReducer。 一个典型信号是:你写了三四个 useState,而且它们之间的更新有依赖关系,比如"打开弹窗时要重置表单、关闭时要清空错误"。这时候 useReducer 比一堆 useState 清晰得多:

tsx
type State = { step: number; data: FormData | null; error: string | null };
type Action =
  | { type: 'NEXT'; payload: FormData }
  | { type: 'RESET' }
  | { type: 'ERROR'; payload: string };

function wizardReducer(state: State, action: Action): State {
  switch (action.type) {
    case 'NEXT':
      return { ...state, step: state.step + 1, data: action.payload, error: null };
    case 'RESET':
      return { step: 0, data: null, error: null };
    case 'ERROR':
      return { ...state, error: action.payload };
    default:
      return state;
  }
}

function MultiStepForm() {
  const [state, dispatch] = useReducer(wizardReducer, { step: 0, data: null, error: null });
  // ...
}

注意一个判断点:useReducer 的 state 如果只在本组件用,就不要再配 useContext。 很多人一见 reducer 就条件反射地套一个 Context Provider,把状态广播出去——结果就是每次 dispatch,整棵订阅子树都在重渲染。这就是典型的"过度设计"。


四、Context:跨组件共享,但别滥用

Context 解决的是 prop drilling(属性逐层透传)的问题。当某个状态需要在多层组件之间共享,但又不至于大到要全局管理时,Context 是最自然的解法。最经典的两个场景:主题(theme) 和 当前登录用户(user)。

tsx
const ThemeContext = createContext<{ theme: Theme; toggle: () => void } | null>(null);

function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');
  const toggle = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
  return ctx;
}

Context 的甜区是低频变化的全局值——主题切换、语言切换、用户身份、特性开关。这些东西变化的频率很低,即使所有消费者在变化时一起重渲染,也几乎感受不到性能代价。

但 Context 有一个被反复忽视的陷阱:Context 的 value 每次变化,所有消费它的组件都会重渲染,无论你用到了 value 里的哪个字段。 这意味着如果你把一个高频变化的值(比如输入框的当前文本、鼠标位置、滚动条位置)塞进 Context,整棵订阅树会跟着疯狂重渲染,性能直接崩。

所以这里有一条硬规则:Context 不要承载高频变化的状态。 一旦你发现 Context 的 value 在每次用户交互时都在变,就该考虑两件事:

  1. 拆 Context:把高频字段和低频字段拆到两个 Context 里,消费者各取所需。
  2. 换 Zustand:用选择器订阅(selective subscription),只在自己关心的字段变化时才重渲染。

另外,Context 是就近共享的工具,不是"全局唯一"。一个 Provider 可以包在某个子树外面,只服务那一小块。不要一上来就把所有 Context 都堆在 App 根节点上——按作用域就近放,组件边界更清晰,也方便测试。


五、Zustand:多数项目的甜区

如果你的全局状态开始变复杂——比如一个购物车,既要存商品列表、又要算总价、又要支持撤销;或者一个多 Tab 编辑器,需要在多个不相邻的组件之间共享编辑状态——这时候 Context 会显得力不从心(主要因为性能和样板代码),Redux 又太重。Zustand 正好是中间那个甜区。

它的优点非常直接:

  • API 极简:一个 create 函数搞定,没有 action types、没有 reducers、没有 dispatch。
  • 选择器订阅:组件可以只订阅自己关心的字段,其他字段变了不会触发重渲染。这是 Context 没有的能力。
  • 不侵入组件树:不需要 Provider 包裹,任何组件直接 useStore() 即可。
ts
import { create } from 'zustand';

interface CartItem { id: string; name: string; price: number; qty: number; }

interface CartStore {
  items: CartItem[];
  add: (item: CartItem) => void;
  remove: (id: string) => void;
  clear: () => void;
  total: () => number;
}

const useCart = create<CartStore>((set, get) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
  remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
  clear: () => set({ items: [] }),
  total: () => get().items.reduce((sum, i) => sum + i.price * i.qty, 0),
}));

消费端的关键写法——只订阅你需要的字段:

tsx
// 只在 items 变化时重渲染
const items = useCart((s) => s.items);
const add = useCart((s) => s.add);

// 不要这样写,会让组件在任何状态变化时都重渲染
// const { items, add, remove, clear } = useCart();

这一行选择器就是 Zustand 比 Context 强的地方。在 Context 里,你很难做到"只有 items 变了我才更新"——你要么手写一套 memo + context 分割,要么直接认命全量重渲染。Zustand 把这件事做成了默认行为。

什么时候选 Zustand? 我的标准是:

  • 状态需要在多个不相邻的组件之间共享(没法用 prop 透传,也不适合用一个 Context 圈起来)。
  • 状态变化频率较高(比如购物车、富文本编辑器状态、复杂表单聚合)。
  • 状态逻辑有一定的复杂度,但还没到"需要一个完整框架"的程度。

这三条同时满足,Zustand 基本就是答案。对绝大多数中型应用来说,Zustand + React Query 这对组合就足够撑起整个状态层了。


六、Redux:重,选它要有理由

Redux 不是不好,是绝大多数项目用不到它那么重。

Redux 的真正价值在两件事:

  1. 可预测的状态演化:action → reducer → new state 这条链路是单向且可追溯的,配合中间件(redux-thunk、redux-saga)可以处理非常复杂的副作用编排。
  2. 时间旅行调试:DevTools 能让你回放每一次状态变更,定位问题极其高效。

但代价是:样板代码多、学习曲线陡、改动成本高。一个简单的"加一个字段"在 Zustand 里是改一处,在经典 Redux 里可能要动 action type、action creator、reducer 三处(虽然 Redux Toolkit 已经把这层样板大幅压缩了,但思维模型仍然是重的)。

所以 Redux 真正适合的是:超大型应用、多人团队协作、对状态可追溯性有严格要求(金融、医疗、复杂工作流)。这种场景下,Redux 的约束力反而是优势——它强制所有人按同一套范式写代码,避免 Zustand 那种"每个 store 写法都不一样"的随意性。

如果你不确定要不要 Redux,那基本就是不需要。用不准的工具比不用工具更糟,Redux 尤其如此。


七、服务端状态用 React Query/SWR,别塞进 Redux

这是近年来 React 社区最重要的认知升级之一:服务端状态和客户端状态是两种完全不同的东西,不该用同一个工具管。

服务端状态有几个客户端状态没有的特点:

  • 它是异步的(loading / success / error 三态)。
  • 它需要缓存(同一个接口在多个地方用到,不该请求多次)。
  • 它会过期(数据可能被别人改了,你需要失效重取)。
  • 它需要乐观更新和失败回滚。
  • 它要处理分页、无限滚动、竞态。

你用 Redux 管这些,等于自己手写一个 React Query——而且大概率写得不如它好。React Query 把上面这些全做成了内置能力:

ts
function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    staleTime: 60_000, // 1 分钟内不重新请求
  });

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage />;

  return data.map((u) => <UserRow key={u.id} user={u} />);
}

变更(mutations)和自动失效也是声明式的:

ts
const mutation = useMutation({
  mutationFn: (newUser) => api.createUser(newUser),
  onSuccess: () => {
    // 创建成功后,自动让 users 列表失效并重新拉取
    queryClient.invalidateQueries({ queryKey: ['users'] });
  },
});

SWR 的定位类似,API 更精简,适合中小项目;React Query 功能更全(突变、依赖查询、并行查询、分页),适合复杂场景。两者选哪个都行,关键是别再自己手撸 loading / error / refetch 了。

把服务端状态交给 React Query 之后,你的 Redux / Zustand 就能瘦身一大半——它们只需要管真正的客户端状态(UI 状态、表单聚合、本地编辑中的草稿)。这才是它们本来该干的活。


八、选型决策树

把前面七节浓缩成一条可执行的决策链。新加一个状态时,按顺序问自己:

  1. 这是服务端数据吗?(来自接口、需要缓存/失效/重试)→ React Query / SWR,到此为止,不要进任何客户端状态库。
  2. 只有当前组件用吗? → useState。多个相关字段联动就上 useReducer。
  3. 需要在一棵子树内共享吗?(而且变化频率低)→ Context,Provider 就近包在子树外。
  4. 需要在多个不相邻组件之间共享、且变化频繁? → Zustand(默认推荐),除非团队已有 Redux 沉淀。
  5. 超大型应用、需要时间旅行调试、强约束团队协作? → Redux Toolkit。

注意这条决策链的顺序本身就是答案:从最轻的开始,只在它不够时才往上升级。 不要跳级。一个 useState 就能解决的问题,不要因为"将来可能要共享"就提前上 Zustand——YAGNI(You Aren't Gonna Need It)在状态管理里特别重要,过早抽象比晚抽象更难拆。

还有一个常被忽略的中间方案:状态提升(state lifting)。两个兄弟组件要共享状态?先把状态提到它们共同的父组件,用 prop + callback 传下去。只有当父组件层级太深、透传成本真的很高时,才考虑 Context 或外部 store。不要一见共享就跳到全局库。


小结

  1. 按层级选工具,不要按"哪个强"选。 状态有四层:本地、跨组件、全局、服务端,每一层都有它最合适的工具。
  2. 本地状态优先 useState / useReducer。 够用就别加库,reducer 不一定要配 Context。
  3. Context 适合低频全局值(theme、user、i18n),不要塞高频变化的状态,否则全树重渲染。
  4. Zustand 是多数项目的甜区——API 简单、选择器订阅性能好,中型应用的全局状态首选。
  5. Redux 留给超大型应用和强约束场景,大多数项目用它是过度工程。
  6. 服务端状态交给 React Query / SWR,缓存、失效、重试、loading 这些不该手写,Redux / Zustand 不该背这个锅。

金句:状态管理不是选最强的工具,而是选最轻的那个能解决问题的工具。 真正的高级工程师,不是把简单问题搞复杂,而是让复杂问题看起来简单。

Related

相关推荐