React 状态管理实践:先分清"谁的 state",再谈要不要全局 store

举报
yd_210610071 发表于 2026/11/03 23:48:36 2026/11/03
【摘要】 React 状态管理实践:先分清"谁的 state",再谈要不要全局 store 前言"要不要上 Redux"是个伪命题。真正的问题是:这份状态属于谁、活多久、谁负责同步。分不清这一点,换十个库也治不好渲染问题和缓存不一致。 一、状态四分法类型典型例子归属推荐方案局部 UI 状态弹窗开关、表单输入组件自身useState / useReducer跨组件 UI 状态主题、侧边栏折叠、购物车应...

React 状态管理实践:先分清"谁的 state",再谈要不要全局 store

前言

"要不要上 Redux"是个伪命题。真正的问题是:这份状态属于谁、活多久、谁负责同步。分不清这一点,换十个库也治不好渲染问题和缓存不一致。

一、状态四分法

类型 典型例子 归属 推荐方案
局部 UI 状态 弹窗开关、表单输入 组件自身 useState / useReducer
跨组件 UI 状态 主题、侧边栏折叠、购物车 应用级 Context(低频)/ Zustand(高频)
服务端状态 列表、详情、用户信息 后端 TanStack Query / SWR
URL 状态 筛选条件、分页、Tab 浏览器 searchParams

这张表能解释大部分"状态管理很难"的错觉:80% 的难题是把服务端状态塞进了全局 store。一旦这么做,loading、error、请求去重、缓存失效、竞态覆盖,全都要自己手写。

二、服务端状态:别手写缓存

import { useQuery } from '@tanstack/react-query';

function UserList({ page }: { page: number }) {
  const { data, isPending, error } = useQuery({
    queryKey: ['users', page],              // key 就是缓存身份
    queryFn: ({ signal }) =>
      fetch(`/api/users?page=${page}`, { signal }).then((r) => r.json()),
    staleTime: 30_000,                      // 30s 内视为新鲜,不重复请求
  });

  if (isPending) return <Skeleton />;
  if (error) return <ErrorBox error={error} />;
  return <Table rows={data.list} />;
}

三个要点:

  1. 参数必须进 queryKey:['users', page] 换个 page 就是另一份缓存,这是自动去重的唯一依据;
  2. 分清 staleTime 和 gcTime:前者决定"多久不重新请求",后者决定"没人用了多久后销毁缓存",两个概念混用会导致"数据莫名变空";
  3. 竞态交给库:signal 让旧请求在组件卸载或 key 变化时被取消,不用自己写 let ignore = false。

写操作靠失效,而不是手动改缓存:

const mutation = useMutation({
  mutationFn: updateUser,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
});

乐观更新则用 onMutate 先改缓存、onError 回滚、onSettled 重新校验,三步齐全才不会出现"界面显示了但服务端没存"。

三、客户端状态:Zustand 的边界

import { create } from 'zustand';

type CartState = {
  items: CartItem[];
  add: (item: CartItem) => void;
};

export const useCart = create<CartState>()((set) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
}));

// 选择器粒度 = 重渲染范围
const count = useCart((s) => s.items.length);

关键纪律:

  • 永远带选择器,useCart() 直接拿整个对象等于订阅全量变更,任何字段变动都重渲;
  • 按业务域拆 store,别做"一个 AppStore 装下所有东西",那是把全局变量换了个名字;
  • store 里不放服务端数据,需要聚合时用选择器派生,或直接读 Query 缓存。

四、Context 不是状态管理器

Context 只解决"跨层透传",不解决"更新效率"。Provider 的 value 一变,所有消费者都会重渲染,哪怕它只用到其中一项。

// ❌ 每次 render 都是新对象,所有消费者全量重渲
<ThemeContext.Provider value={{ theme, setTheme }}>

// ✅ 拆成两个 context,或对 value 做 memo
const value = useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>

适用边界很明确:低频变化(主题、语言、当前用户)。高频更新(输入、拖拽、动画)不要走 Context。

五、URL 也是状态源

筛选条件、分页、当前 Tab 放进 URL 带来三个白拿的好处:刷新可恢复、链接可分享、后退可回到上一组条件。

const [params, setParams] = useSearchParams();
const page = Number(params.get('page') ?? 1);

setParams({ page: '2' }, { replace: true }); // 别让每次筛选都进历史栈

判据:这个状态是否应该被"记住"或"分享"。是,就进 URL;不是,留在内存。

六、五个常见反模式

  1. 用一个巨型 store 存所有状态:更新任意字段,订阅者全部重渲;
  2. useEffect 里同步两个状态源:setState 依赖另一个 state,几乎总是能用"单一数据源 + 派生"消灭;
  3. 把 props 复制进 state:父级更新后子级不同步,直接实时计算;
  4. useMemo 滥用:基础类型比较的开销可能大于计算本身,只在依赖昂贵计算或引用稳定时用;
  5. 跨模块直接读别人的 store:模块间耦合,重构时牵一发动全身,改用 props / 事件 / 接口层。

总结

决策顺序就一句话:能放组件内就不外提,能放 URL 就不进内存,服务端数据交给 Query,剩下的 UI 状态才考虑 Zustand,低频跨层用 Context。

状态管理的复杂度从来不是库造成的,而是"同一份数据有了两个主人"。让每份状态只有一个归属,代码自然就顺了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。