React 项目中如何选择状态管理方案?
简化版
React 状态管理要先区分状态类型:本地 UI 状态放组件内,跨层级共享可用 Context,复杂客户端状态可用 Redux/Zustand/Jotai,服务端数据缓存应考虑 React Query、SWR 等。不要把所有状态都塞进全局 store。
详细版
常见分类:
- 组件本地状态:弹窗开关、输入框值,用
useState。 - 局部复杂状态:复杂表单、多步骤流程,用
useReducer。 - 跨层级共享状态:主题、语言、当前用户,用 Context。
- 大型客户端状态:复杂业务 store,用 Redux、Zustand 等。
- 服务端状态:接口数据、缓存、刷新、失效,用 React Query、SWR。
面试重点是说明“状态类型决定方案”,而不是直接说某个库最好。
完整版教学
一、先区分状态类型,而不是先选库
状态管理最常见的问题是上来就问“用 Redux 还是 Zustand”。 真正的工程判断应该先问:这个状态属于谁、生命周期多长、是否跨组件、是否来自服务端。 不同状态有不同归宿。
状态来源
-> 组件内部临时状态
-> 客户端共享状态
-> 服务端远程数据
-> URL 状态
例如搜索关键词可能应该进 URL query,而不是全局 store。 接口列表数据可能应该交给服务端状态库,而不是手写 Redux 缓存。 分类正确,方案自然会清楚。
二、本地 UI 状态优先留在组件内
弹窗开关、hover、输入框临时值、Tab 当前项,这些通常只影响一个组件或局部子树。
用 useState 或 useReducer 放在组件内最简单。
放进全局 store 会扩大影响范围。
function DialogButton() {
const [open, setOpen] = useState(false);
return <button onClick={() => setOpen(true)}>打开</button>;
}
如果 1 个组件使用,就不要做成全局。 如果 3 到 5 个兄弟组件共享,可以先考虑状态提升到共同父组件。 只有跨很多层或跨页面时,再考虑 Context 或外部 store。
三、Context 适合低频共享,不适合高频大状态
Context 适合主题、语言、当前登录用户、权限能力这类低频共享数据。 它解决的是“避免层层传 props”。 但 Context value 变化会让消费它的组件重新渲染,复杂高频状态直接塞 Context 可能带来性能问题。
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
| 状态 | 是否适合 Context | 原因 |
|---|---|---|
| theme | 适合 | 低频、全局读取 |
| locale | 适合 | 低频、全局读取 |
| 鼠标坐标 | 不适合 | 高频变化 |
| 大型表格数据 | 谨慎 | 更新范围大 |
如果 Context value 每秒更新 60 次,很多消费者都会受影响。 这时应该考虑拆 Context、使用 selector 型 store,或保持状态局部化。
四、复杂客户端状态适合外部 store
当状态跨页面共享、更新动作复杂、需要 devtools、持久化、中间件或更细粒度订阅时,可以考虑外部状态库。 Redux 生态成熟、约束强;Zustand 更轻量;Jotai/Recoil 更偏原子化模型。 没有绝对最好,只有适合场景。
Redux:
大型团队、强约束、复杂调试
Zustand:
轻量 store、低样板代码
Jotai:
原子状态、细粒度组合
数字例子:如果一个后台有 20 个业务模块、50 多个跨页面状态、需要审计 action,Redux 的约束可能更有价值。 如果只是几个全局状态,Zustand 可能更轻。 选型要看团队规模和状态复杂度。
五、服务端状态不应该简单当成本地全局状态
接口数据和本地 UI 状态很不一样。 它有加载中、错误、缓存、重新请求、失效、并发、分页、乐观更新等问题。 这类状态常被称为 server state。
服务端状态问题:
loading / error
cache / stale
refetch
pagination
optimistic update
如果手写全局 store 管所有接口缓存,很容易越写越复杂。 React Query、SWR 这类库就是为服务端状态设计的。 面试回答中把 client state 和 server state 分开,是一个很重要的加分点。
六、URL 状态适合表达可分享的页面状态
分页、筛选、排序、搜索词有时应该放进 URL。 这样刷新页面不丢,链接可以分享,也更利于回退前进。 不要把所有页面状态都放内存 store。
/users?page=2&keyword=react&role=admin
记忆钩子:组件状态问“谁用”,服务端状态问“谁拥有”,URL 状态问“要不要分享”。
如果状态需要用户复制链接后仍然保留,就优先考虑 URL。 如果只是弹窗开关,就没必要污染 URL 或全局 store。
七、常见误区与追问
- 误区:React 项目都必须上 Redux。 小项目或局部状态多的项目不一定需要 Redux,过早引入会增加样板代码。
- 误区:Context 可以替代所有状态库。 Context 适合低频共享,高频复杂状态可能导致性能和组织问题。
- 误区:接口数据放 Redux 就是最佳实践。 服务端状态有缓存和失效问题,React Query、SWR 等工具更专业。
- 追问:什么时候状态提升就够了? 只有少量兄弟组件共享时,把状态提到共同父组件通常比全局 store 简单。
- 追问:如何判断状态是否全局? 看是否跨页面、是否多处读写、是否需要持久化或统一调试。
- 追问:为什么 URL 也是状态管理? URL 能表达可分享、可刷新恢复的页面状态,如分页、筛选和排序。
八、加强记忆
这题不要从库名开始答,要从状态分类开始答。本地 UI 状态留组件内,复杂局部状态用 reducer,低频跨层共享用 Context,大型客户端状态用外部 store,接口缓存用服务端状态库,可分享筛选分页放 URL。能这样分类,选型就很稳。