React 状态边界:不是所有状态都该放进全局 Store

📅 2026/7/3 1:56:24
React 状态边界:不是所有状态都该放进全局 Store
React 状态边界不是所有状态都该放进全局 Store一、全局状态用多了组件会失去边界React 项目变大后很多团队会把状态不断塞进全局 Store。用户信息、筛选条件、弹窗开关、表格选中项、表单草稿、请求状态最后全都在一个巨大状态树里。这样做早期很方便但后面会带来渲染范围扩大、依赖关系混乱、测试困难和状态泄漏。状态管理的重点不是选 Redux、Zustand 还是 Jotai而是先判断状态属于哪里。局部状态、页面状态、跨页面状态、服务端缓存和 URL 状态生命周期完全不同。把它们都放到全局 Store就像把所有东西都塞进一个抽屉找的时候一定会乱。二、状态分类生命周期决定存放位置flowchart TD A[前端状态] -- B[组件局部状态] A -- C[页面级状态] A -- D[URL 状态] A -- E[服务端缓存] A -- F[全局业务状态]组件内部的展开收起、输入框临时值、hover 状态通常放在组件里。页面筛选条件如果需要刷新后保留或分享链接适合放进 URL。接口数据更像服务端缓存React Query 或 SWR 这类工具比手写全局状态更合适。真正的全局状态应是登录用户、主题、权限、跨页面任务等少量核心信息。判断标准可以很简单这个状态是否需要被多个远距离模块同时读写刷新后是否应该保留是否能从服务端重新获取是否属于 URL 语义问完这些问题很多状态其实不该进全局 Store。在实际项目中我们经常看到一种反模式把弹窗开关塞进全局 Store。比如一个编辑弹窗的visible状态开发者为了在不同地方调用把它放到了全局。结果每次切换弹窗全局 Store 更新所有订阅者都重新渲染// 不推荐弹窗状态放全局 const useGlobalStore create((set) ({ editModalVisible: false, setEditModalVisible: (v: boolean) set({ editModalVisible: v }), }));这会导致无关组件也跟着 re-render。更好的做法是用 URL hash 或者组件内部状态加局部 Context// 推荐弹窗状态放在共享的模块级 Context 中 const EditModalContext createContextEditModalAPI | null(null); function EditModalProvider({ children }: { children: ReactNode }) { const [visible, setVisible] useState(false); // Provider 只包裹需要弹窗的组件树而不是整个应用 return ( EditModalContext.Provider value{{ visible, open: () setVisible(true), close: () setVisible(false) }} {children} EditModal visible{visible} onClose{() setVisible(false)} / /EditModalContext.Provider ); }这样弹窗状态只在需要它的子树中流转不会污染全局 Store也不会触发全量渲染。如果多个页面都需要类似弹窗可以抽成通用的DialogManager但依然保持局部作用域。三、代码示例筛选条件放进 URL下面是一个简化示例用 URL 管理列表筛选条件。这样刷新和分享链接都能保留状态。import { useSearchParams } from react-router-dom; export function useOrderFilter() { const [params, setParams] useSearchParams(); const status params.get(status) ?? all; function setStatus(next: string) { const copy new URLSearchParams(params); copy.set(status, next); setParams(copy); } return { status, setStatus }; }如果把这个筛选条件放进全局 Store用户刷新后状态丢失复制链接也无法复现当前列表。URL 本来就是页面状态的一部分善用它能少写很多同步逻辑。服务端数据也类似。不要把接口返回的列表手动塞进 Store再自己处理 loading、error、缓存失效和重试。成熟的数据请求库已经解决了这些问题。全局 Store 应该保存业务决策不应该变成简陋缓存层。在复杂表单场景中我们还遇到过草稿放在全局 Store 导致的状态泄漏问题。用户在表单页填了一半切到其他页面回来后草稿消失了——因为全局 Store 在路由切换时被重置。正确做法是利用浏览器的beforeunload或 SessionStoragefunction useFormDraft(key: string) { const [draft, setDraft] useState(() { const saved sessionStorage.getItem(draft:${key}); return saved ? JSON.parse(saved) : {}; }); useEffect(() { sessionStorage.setItem(draft:${key}, JSON.stringify(draft)); }, [draft, key]); return { draft, setDraft }; }这样草稿只在当前浏览器会话中保留切换路由不受影响关闭浏览器后自动清理。比全局 Store 更适配草稿的生命周期。四、工程实践状态越靠近使用处越好状态应该尽量靠近使用它的组件。只有当它确实需要跨边界共享时再上移或放入全局。这样组件更容易测试也减少无关渲染。很多性能问题不是 React 慢而是状态放太高导致一处变化牵动半个页面。对于复杂页面可以按业务区域拆状态边界。例如列表筛选区、详情侧栏、批量操作栏分别管理自己的状态通过明确事件通信。不要用一个大 Store 让所有模块互相读写。读写越自由维护越痛苦。最后状态命名要表达生命周期。temporaryDraft、urlFilter、serverCache、globalSession这类名字比data、info更能提醒维护者该怎么处理它。代码优雅有时不是抽象高级而是边界清楚。一个实用检查技巧在做 Code Review 时对每个出现在全局 Store 里的字段问三个问题1. 这个字段会被 2 个以上彼此无关的页面使用吗 2. 用户在页面之间跳转时这个字段应该保留吗 3. 这个字段有没有明确的所有者团队/模块如果答案不清晰就把字段挪回更小的作用域。这个检查可以做成简单的评审规则甚至写进 CI 里。我们团队就加了一条 ESLint 自定义规则当组件引用了超过 5 个全局 Store 字段时发出警告。不是强制的但足以让开发者停下来想一想。五、总结React 状态管理先看生命周期再选工具。局部状态留在组件页面语义放 URL服务端数据交给缓存库真正跨页面共享的少量信息才进全局 Store。状态越靠近使用处应用越容易维护。