Zustand 全局状态管理
以下集成内容对当前项目内Zustand的实际使用进行说明,如需进行代码对照,可参考目录下 react19_ts 项目内实际代码,文档与代码会在git上持续补充
git仓库地址:https://gitee.com/xiaoli-account/react19_ts> 简单、轻量、没有样板代码,使用方便。
Store 目录结构
> 当前项目分两个位置存放store,分别是 `src/store/` 和 `src/layout/stores/`, 其中 `src/store/` 存放业务相关的全局状态,`src/layout/stores/` 存放布局相关的状态。
src/├── store/ # 业务相关的全局状态│ ├── index.ts # 统一导出,方便其他地方 import│ ├── user.ts # 用户信息、登录状态、Token、权限│ └── app.ts # 应用级的状态,比如全局 loading└── layout/ └── stores/ # 布局相关的状态 ├── index.ts # 统一导出 ├── layout-store.ts # 布局模式、侧边栏收起来还是展开 ├── i18n-store.ts # 语言切换 ├── menu-store.ts # 菜单数据 ├── tabs-store.ts # 标签页 ├── theme-store.ts # 主题(暗黑/明亮模式) ├── router-store.ts # 路由配置 └── switch-system-store.ts # 系统切换
建议:业务状态放 `src/store/`,布局相关的放 `src/layout/stores/`,别混着放,后期维护方便。
在组件里怎么用?
这是比较常用的位置,比如在页面组件中使用
import { useUserStore } from "@/store";const UserCard = () => { // 直接解构出来用,简单直接 const { userInfo, logout } = useUserStore(); return ( <div> <p>欢迎,{userInfo?.nickname}</p> <buttononClick={logout}>退出登录</button> </div> );};
在普通 TS/JS 文件里怎么用?
有时候咱们需要在工具函数、axios 拦截器、或者其他非 React 组件的地方读取/修改状态。这时候不能用 Hook,得用 `getState()`:
import { useUserStore } from "../store/user";// 在 axios 拦截器里拿 tokenaxios.interceptors.request.use((config) => { const token = useUserStore.getState().getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config;});// 重置用户状态(比如退出登录后清空所有数据)useUserStore.getState().resetUserStore();
⚠️注意⚠️:千万别在组件外直接 `useUserStore()`,这样用会报错。组件外用 `getState()`,组件内用 Hook。
项目里现有的 Store 使用方式介绍
1. User Store - 用户信息
路径:`src/store/user.ts`
这个 store 存放用户信息相关,登录、登出、Token、权限都在这儿。
状态变量:
- `userInfo` - 用户信息,头像、昵称、部门什么的
- `isLogin` - 是否已登录
- `token` - 登录凭证,同步会在cookie中与localStorage中存储
- `pages/apis/btns` - 权限列表,控制你能看到哪些页面、调用哪些接口、点哪些按钮
- `roles` - 角色列表
常用方法:
- `login(loginInfo)` - 登录,调用后端接口,成功后存 token
- `logout()` - 登出,清掉所有用户数据
- `setUserInfo(userInfo)` - 设置用户信息
- `getToken()` - 拿 token,很多地方会用到
- `resetUserStore()` - 一键清空用户状态,退出登录时调用
2. Layout Store - 布局控制
路径:`src/layout/stores/layout-store.ts`
控制页面布局相关的状态。
布局模式:
- `layoutMode` - 布局模式,咱们有 4 种:`lee-basic`、`lee-sidebar`、`lee-top-menu`、`lee-plm`
- `sidebarCollapsed` - 侧边栏是展开还是收起
切换布局示例:
const { layoutMode, setLayoutMode, toggleSidebar } = useLayoutStore();// 切换布局setLayoutMode('lee-sidebar');// 收起/展开侧边栏toggleSidebar();
3. I18n Store - 语言切换
路径:`src/layout/stores/i18n-store.ts`
管理项目国际化,中文/英文切换。
const { locale, setLanguage } = useI18nStore();// 切换到英文setLanguage('en-US');
切换后会自动更新 HTML 的 lang 属性,也会同步 i18next 的语言设置。
Store 编写完整示例
给你看看咱们项目里一个 store 是怎么写的,以后你照着这个模板写就行:
import { create } from 'zustand';import { persist } from 'zustand/middleware';import i18n from '../i18n';// 1. 先定义接口,别偷懒,TypeScript 检查能帮你省很多 bugexport interface I18nState { locale: 'zh-CN' | 'en-US'; setLanguage: (locale: 'zh-CN' | 'en-US') => void; getI18nByKey: (key: string) => string;}// 2. create 创建 storeexport const useI18nStore = create<I18nState>()( // 3. 用 persist 包裹,状态就能自动存到 localStorage persist( (set) => ({ // 初始值 locale: 'zh-CN', // 方法里用 set 修改状态 setLanguage: (locale) => { set({ locale }); document.documentElement.lang = locale; if (i18n.language !== locale) { void i18n.changeLanguage(locale); } }, getI18nByKey: (key: string) => { return i18n.t(key); }, }), { // localStorage 里的 key name: 'layout-i18n-storage', // 只存 locale,其他不存 partialize: (state) => ({ locale: state.locale, }), } ));// 3、create 创建 非持久化 storeexport const useRouterStore = create<RouterState>((set, get) => ({ // 初始路由配置 routes: [], // 更新路由配置 setRoutes: (newRoutes: RouteItem[]) => { LeeLogger.info("更新路由配置", newRoutes); set({ routes: newRoutes }); // 更新菜单数据 useMenuStore.getState().setRoutesMenu(newRoutes); }, // 获取路由配置 getRoutes: () => { return get().routes; }, // 重置路由配置 resetRoutes: (routes: RouteItem[]) => { LeeLogger.info("重置路由配置", routes); set({ routes: routes }); },}));
写 store 的三个步骤:
1. 定义接口
2. create 创建
3. persist 持久化(如果需要的话)
状态持久化 - 刷新页面数据不丢
用了 `persist` 中间件,状态会自动存到 localStorage,刷新页面后数据还在。
import { persist, createJSONStorage } from "zustand/middleware";export const useStore = create<StoreState>()( persist( (set, get) => ({ // 你的状态和方法 }), { // localStorage 的 key,别跟其他 key 冲突 name: "my-app-storage", // 存储引擎,默认就是 localStorage storage: createJSONStorage(() => localStorage), // 只存这些字段,其他的不存(节省空间,也安全) partialize: (state) => ({ userInfo: state.userInfo, token: state.token, }), } ));
使用注意⚠️:
- `name` 要全局唯一,别跟其他 store 重复
- `partialize` 很重要,**别存敏感信息**,比如密码、密钥之类的
- 如果存的东西比较多,考虑加 `version` 字段做迁移(persist 支持版本控制)
响应式 & 状态监听 - 状态变了自动刷新
1. 组件里自动响应
Zustand 是响应式的,组件里用了什么状态,那个状态变了组件就会自动重渲染。
// 只订阅 userInfo,token 变了不会触发重渲染const { userInfo } = useUserStore();// 用选择器精确订阅,性能更好const token = useUserStore((state) => state.token);const nickname = useUserStore((state) => state.userInfo?.nickname);
2. 在 useEffect 里监听状态变化 (subscribe)
有时候你需要在状态变化时做点额外的事情,比如记录日志、发请求什么的。这时候用 `subscribe`:
useEffect(() => { // subscribe 返回一个取消订阅的函数 const unsubscribe = useUserStore.subscribe((state) => { const { loginName, id } = state.userInfo || {}; if (loginName && id) { LeeLogger.setCurrentUser(loginName, id); } else { LeeLogger.clearCurrentUser(); } }); // 组件卸载时取消订阅,防止内存泄漏 return () => unsubscribe();}, []);
项目里的实际例子: `src/layout/router/index.tsx` 里用 subscribe 监听用户登录状态,登录后自动更新日志系统的用户信息。
3. 常用监听场景
| | |
| | `const { userInfo } = useUserStore()` |
| | |
| | |
| | |
persist 支持版本控制
当 store 结构发生变化时(比如增删字段、重命名字段),已经存在用户 localStorage 里的旧数据可能跟新代码不兼容。这时候可以用 `version` 和 `migrate` 来做数据迁移。
import { persist, createJSONStorage } from "zustand/middleware";export const useUserStore = create<UserState>()( persist( (set, get) => ({ // 状态定义... }), { name: "user-storage", storage: createJSONStorage(() => localStorage), // 当前数据版本号,每次结构变更时递增 version: 2, // 数据迁移函数 migrate: (persistedState, version) => { // 从 version 1 升级到 version 2 if (version === 1) { return { ...persistedState, // 新增字段给个默认值 newField: "defaultValue", // 重命名字段:把 oldName 改成 newName newName: persistedState.oldName, oldName: undefined, // 清理旧字段 }; } // 如果版本匹配不上,直接返回当前状态(会丢失旧数据) return persistedState; }, } ));
什么时候需要版本控制:
- 字段改名(比如 `nickname` 改成 `displayName`)
- 数据结构变化(对象改成数组)
- 新增必填字段(给个默认值)
- 删除废弃字段(清理旧数据)
建议:上线前先在本地测试 migrate 逻辑,确保用户数据能平滑升级,别让用户登录后数据丢了。
Zustand 使用建议
1. Store 别太大
一个 store 管一类事,别把所有状态都塞一个文件里。项目里分了 `user`、`app`、`layout`、`menu` 等,每个职责清晰。
2. 接口定义别偷懒
TypeScript 接口写全了,方便阅读与维护。特别是复杂的状态结构,定义清楚能省很多调试时间。
3. 敏感信息别存 localStorage
`partialize` 可以进行持久化缓存,但不要乱存、全存,只存必要的数据、关键的数据。
4. 异步操作放 store 里
登录、获取用户信息这些异步操作,直接写在 store 的方法里,组件里调用就一行代码,逻辑集中好维护。
// store 里封装好login: async (loginInfo) => { const res = await api.login(loginInfo); set({ token: res.data, isLogin: true });}// 组件里直接用const { login } = useUserStore();await login({ username, password });
5. 状态更新用展开运算符
更新对象/数组时记得用展开运算符,别直接改原对象(虽然 Zustand 不会报错,但可能有坑)。
// 正确set({ userInfo: { ...userInfo, nickname: '新昵称' } });// 错误(虽然能跑,但不推荐)userInfo.nickname = '新昵称';set({ userInfo });
6. 组件外用 getState()
组件外用 `useUserStore.getState()`,别直接调 Hook,会报错。
结语
Zustand 上手是真的快,轻量,使用方便,易于理解,推荐使用。