全局状态、局部状态管理
- Requirement:状态按作用域分为三层,禁止跨层滥用。
| 层级 | 作用域 | 管理方式 | 示例 |
|---|
| 局部状态 | 单个组件内 | useState / useReducer(React)、ref / reactive(Vue) | 表单输入、弹窗开关、loading |
| 共享状态 | 多个组件共享 | 全局 Store(Pinia / Redux / Zustand) | 用户信息、权限、主题 |
| 服务端状态 | 来自后端的数据 | 请求库缓存(TanStack Query / SWR) | 列表数据、详情数据 |
- Requirement:优先使用局部状态,只有在多个组件需要共享时才提升为全局状态。
- Standard:服务端数据(来自接口)不放入全局 Store,使用 TanStack Query / SWR 等请求库管理缓存。
- Caution:禁止将页面的临时筛选条件、表单草稿放入全局 Store,刷新后会丢失且污染全局。
│ ├── 是 → 用 TanStack Query / SWR 管理
│ └── 否 → 组件局部状态(useState / ref)
| 反模式 | 问题 | 建议 |
|---|
| 所有状态都放全局 Store | Store 膨胀、不必要的重渲染 | 优先局部状态 |
| 接口数据放全局 Store | 缓存管理复杂 | 用请求库管理 |
| 局部状态提升到父组件层层传递 | prop drilling | 用 Context 或 Store |
- Standard:优先使用现代状态管理库:
- React:Zustand(轻量)或 Redux Toolkit(大型应用)
- Vue:Pinia
- Requirement:禁止直接使用
localStorage 作为状态管理,需通过 Store 持久化插件统一处理。
- Standard:按业务模块拆分 Store,每个 Store 包含 state、getters、actions 三部分。
- Standard:Store 文件放在
src/store/ 目录下,按模块命名,如 user.ts、app.ts、permission.ts。
├── index.ts # Store 入口,统一导出
├── app.ts # 应用级状态(主题、语言)
// src/store/user.ts
import { create } from 'zustand';
interface UserState {
id: string;
name: string;
avatar: string;
token: string;
isLoggedIn: () => boolean;
setUser: (user: Partial<UserState>) => void;
clearUser: () => void;
}
const initialState = {
id: '',
name: '',
avatar: '',
token: '',
};
export const useUserStore = create<UserState>((set, get) => ({
...initialState,
isLoggedIn: () => !!get().token,
setUser: (user) => set(user),
clearUser: () => set(initialState),
}));
// src/store/user.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
id: '',
name: '',
avatar: '',
token: '',
}),
getters: {
isLoggedIn: (state) => !!state.token,
},
actions: {
setUser(user: Partial<UserState>) {
Object.assign(this, user);
},
clearUser() {
this.$reset();
},
},
});
- Requirement:组件中通过 selector 精确订阅需要的状态,避免订阅整个 Store 导致不必要的重渲染。
// React:精确选择
const name = useUserStore((state) => state.name);
const setUser = useUserStore((state) => state.setUser);
// 错误:订阅整个 store
const userStore = useUserStore();
- Standard:异步逻辑放在 actions 中,禁止在组件内直接修改 state。
| 反模式 | 问题 | 建议 |
|---|
| 一个大 Store 存所有状态 | 难以维护、重渲染 | 按模块拆分 |
| 组件内直接修改 state | 状态变更不可追踪 | 通过 actions 修改 |
| 订阅整个 Store | 不必要的重渲染 | 使用 selector |
| Store 中存接口请求的列表数据 | 缓存管理困难 | 用请求库管理 |
- Standard:组件私有状态放在组件内部,使用
useState / useReducer(React)或 ref / reactive(Vue)。
- Standard:多个子组件共享的状态提升到最近的共同父组件,或通过 Context 传递。
- Standard:简单状态(开关、计数)用
useState,复杂状态(多字段、有依赖关系)用 useReducer。
// 简单状态用 useState
const [visible, setVisible] = useState(false);
// 复杂状态用 useReducer
interface FormState {
values: Record<string, any>;
errors: Record<string, string>;
submitting: boolean;
}
type FormAction =
| { type: 'SET_VALUE'; field: string; value: any }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'SUBMIT_START' }
| { type: 'SUBMIT_END' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_VALUE':
return { ...state, values: { ...state.values, [action.field]: action.value } };
case 'SET_ERROR':
return { ...state, errors: { ...state.errors, [action.field]: action.error } };
case 'SUBMIT_START':
return { ...state, submitting: true };
case 'SUBMIT_END':
return { ...state, submitting: false };
}
}
- Requirement:派生状态(从其他状态计算得出)使用
useMemo / computed,禁止用额外的 state 存储。
// 正确:派生状态用 useMemo
const [list, setList] = useState<User[]>([]);
const activeCount = useMemo(
() => list.filter((item) => item.status === 'active').length,
[list]
);
// 错误:用额外 state 存派生数据
const [activeCount, setActiveCount] = useState(0);
useEffect(() => {
setActiveCount(list.filter((item) => item.status === 'active').length);
}, [list]);
- Standard:当两个子组件需要共享状态时,将状态提升到它们的共同父组件。
- Caution:如果状态需要跨多层传递,使用 Context 而非逐层传递 props(prop drilling)。
| 反模式 | 问题 | 建议 |
|---|
| 派生状态用额外 state | 数据不同步、多余渲染 | 用 useMemo/computed |
| 用 useEffect 同步状态 | 容易出现无限循环 | 直接在事件中更新 |
| prop drilling 超过 3 层 | 维护困难 | 用 Context 或 Store |
- Standard:需要持久化的状态(用户 Token、主题、语言)通过 Store 持久化插件统一处理,禁止直接操作
localStorage。
- Requirement:敏感信息(Token、用户信息)加密存储,或使用 HttpOnly Cookie,禁止明文存
localStorage。
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useUserStore = create<UserState>()(
persist(
(set, get) => ({
...initialState,
setUser: (user) => set(user),
clearUser: () => set(initialState),
}),
{
name: 'user-store',
partialize: (state) => ({ token: state.token }), // 只持久化必要字段
}
)
);
import { defineStore } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
export const useUserStore = defineStore('user', {
state: () => ({ ... }),
persist: {
key: 'user-store',
paths: ['token'], // 只持久化 token
},
});
| 内容 | 是否持久化 | 存储方式 |
|---|
| 用户 Token | 是 | 加密 localStorage 或 HttpOnly Cookie |
| 主题/语言 | 是 | localStorage |
| 用户基本信息 | 视情况 | 内存为主,刷新后重新获取 |
| 表单草稿 | 否(临时) | 不持久化或 sessionStorage |
| 列表数据 | 否 | 请求库缓存 |
| 反模式 | 问题 | 建议 |
|---|
| Token 明文存 localStorage | XSS 攻击可窃取 | 加密存储或用 Cookie |
| 直接在组件中操作 localStorage | 状态不同步 | 通过 Store 持久化 |
| 持久化整个 Store | 存储冗余数据 | 只持久化必要字段 |
- Requirement:开发环境必须安装并启用状态管理调试工具:
- Redux:Redux DevTools
- Pinia:Vue DevTools(内置 Pinia 支持)
- Zustand:集成 Redux DevTools 中间件
- Standard:生产环境关闭调试工具,避免暴露内部状态。
- Standard:所有状态变更必须通过 actions,便于在 DevTools 中追踪时间线。
- Caution:禁止在组件内直接修改 state 对象(React 的 state 必须不可变)。
- Suggestion:开发环境在 state 变更时输出日志,包含 action 名称、变更前后状态。
// Zustand 日志中间件
const log = (config) => (set, get, api) =>
config(
(args) => {
console.log('[store] before:', get());
set(args);
console.log('[store] after:', get());
},
get,
api
);
| 反模式 | 问题 | 建议 |
|---|
| 生产环境开启 DevTools | 性能损耗、信息暴露 | 生产环境关闭 |
| 直接修改 state 对象 | React 无法检测变更 | 不可变更新 |
| 无调试工具 | 难以定位状态问题 | 安装 DevTools |