怎么在 UmiJS 中管理全局状态和数据流
详细讲解在 UmiJS 中管理全局状态与数据流的实用方法,包括基于 Hooks 的 plugin-model、初始化状态 getInitialState、传统 Dva 方案以及状态选型与性能优化策略。
在 UmiJS 中管理全局状态,核心方案由两层组成:绝大多数日常中后台业务直接使用框架内置的 plugin-model(基于 React Hooks 的轻量模型)搭配 src/app.ts 中的 getInitialState 即可满足;对于极少数拥有长时序、复杂 Saga 异步编排的大型工程或老项目迁移,才需要引入基于 Redux 的 @umijs/plugin-dva。
一、UmiJS 数据流体系的核心机制与架构分工
在传统 React 应用中,引入 Redux 或 MobX 往往需要编写大量的 Action Type、Reducer、Context Provider 与样板配置。UmiJS 针对中后台系统的开发高频场景,将数据流收敛为“约定驱动、开箱即用”的分层架构:
- 应用启动级状态(
@@initialState):负责在应用加载之初拉取当前登录用户信息、权限字典、站点主题等全局基石数据,供导航栏、路由守卫与权限组件直接消费。 - 通用业务级状态(
plugin-model):在src/models目录下编写标准自定义 React Hook,Umi 会自动将其注册为全局单例模型,任意组件通过useModel即可消费与更新。 - 复杂事件流(
plugin-dva):基于 Redux 与 Redux-Saga,适合高并发联动、长事务处理以及需要从老旧 Umi 2/3 工程平滑迁移的业务。
这种分层避免了“所有状态一股脑扔进全局单一 Store”带来的混乱与性能损耗。不同生命周期的数据各司其职,既保证了开发效率,又维持了清晰的职责边界。
二、轻量首选:使用 plugin-model 管理业务全局状态
plugin-model 是 UmiJS 官方推荐的通用全局状态方案。它的核心理念是将大家熟悉的 React Custom Hook 直接提升为全局单例状态,无需学习新的状态机概念。
步骤 1:在 src/models 下创建 Hook 模型
在项目的 src/models 目录下新建文件,例如 useUserModel.ts。模型就是一个普通的自定义 Hook,内部可以使用 useState、useCallback 等标准 React API:
// 示例文件位置:src/models/useUserModel.ts
import { useState, useCallback } from 'react';
export interface UserProfile {
id: string;
name: string;
role: string;
}
export default function useUserModel() {
const [user, setUser] = useState<UserProfile | null>({
id: 'U1001',
name: '张三',
role: 'admin',
});
const [collapsed, setCollapsed] = useState<boolean>(false);
// 模拟登出操作
const logout = useCallback(() => {
setUser(null);
}, []);
// 模拟修改用户名
const updateName = useCallback((newName: string) => {
setUser((prev) => (prev ? { ...prev, name: newName } : null));
}, []);
const toggleCollapsed = useCallback(() => {
setCollapsed((prev) => !prev);
}, []);
return {
user,
collapsed,
logout,
updateName,
toggleCollapsed,
};
}
src/models/ 目录下后,Umi 会以文件名的基础部分作为 model 的命名空间。例如 useUserModel.ts 或 user.ts,在后续使用时对应的 Key 分别为 'useUserModel' 或 'user'。
步骤 2:在组件中订阅与更新模型
在工程内的任何函数组件中,直接从 umi 中导入 useModel,传入模型的名字即可:
// 示例文件位置:src/pages/Header/index.tsx
import React from 'react';
import { useModel } from 'umi';
export default function HeaderComponent() {
// 直接解构获取数据和更新方法
const { user, logout } = useModel('useUserModel');
if (!user) {
return <div>未登录</div>;
}
return (
<header style={{ display: 'flex', justifyContent: 'space-between', padding: 16 }}>
<span>当前用户:{user.name}({user.role})</span>
<button onClick={logout}>退出登录</button>
</header>
);
}
步骤 3:使用 Selector 避免不必要的重渲染
当一个模型中包含多个属性时,默认调用 useModel('modelName') 会导致该模型中任何属性改变时,所有使用了该 Hook 的组件都重新渲染。
为了解决性能瓶颈,useModel 支持传入第二个参数 selector,只有在 selector 返回的字段发生变化时,当前组件才会触发 re-render:
// 示例文件位置:src/pages/Sidebar/index.tsx
import React from 'react';
import { useModel } from 'umi';
export default function SidebarComponent() {
// 仅在 collapsed 改变时重新渲染,user 的变动不会影响该侧边栏
const collapsed = useModel('useUserModel', (model) => model.collapsed);
const toggleCollapsed = useModel('useUserModel', (model) => model.toggleCollapsed);
return (
<aside style={{ width: collapsed ? 80 : 200 }}>
<button onClick={toggleCollapsed}>
{collapsed ? '展开' : '收起'}
</button>
</aside>
);
}
三、应用级初始化数据流:getInitialState 与 @@initialState
多数管理系统在渲染页面前,必须先获取当前登录用户的鉴权 Token、角色信息以及全局配置。如果把这些逻辑写在普通页面的 useEffect 中,很容易出现页面布局闪烁或权限判定滞后。
UmiJS 提供了标准运行时配置文件 src/app.ts,配合 getInitialState 机制专门解决这一问题。
1. 在 app.ts 中定义初始化函数
导出名为 getInitialState 的异步函数。Umi 会在应用挂载的第一时间调用它,并将其返回值全局托管在名为 @@initialState 的特殊 model 中:
// 示例文件位置:src/app.ts
import { queryCurrentUser, querySiteSettings } from '@/services/user';
export async function getInitialState(): Promise<{
currentUser?: API.CurrentUser;
settings?: API.SiteSettings;
}> {
// 检查本地 Token
const token = localStorage.getItem('access_token');
if (!token) {
return {};
}
try {
// 并行请求当前登录人与全局字典
const [currentUser, settings] = await Promise.all([
queryCurrentUser(),
querySiteSettings(),
]);
return {
currentUser,
settings,
};
} catch (error) {
// 异常处理:例如登录失效
localStorage.removeItem('access_token');
return {};
}
}
2. 在业务中消费与动态刷新 @@initialState
通过 useModel('@@initialState'),不仅可以获取初始化的数据,还能获得 setInitialState 和 refresh 方法:
// 示例文件位置:src/pages/Login/index.tsx
import React from 'react';
import { useModel, history } from 'umi';
import { loginAccount } from '@/services/auth';
export default function LoginPage() {
const { initialState, setInitialState, refresh } = useModel('@@initialState');
const handleLogin = async (credentials: any) => {
const res = await loginAccount(credentials);
if (res.token) {
localStorage.setItem('access_token', res.token);
// 方式 1:调用 refresh(),自动重新执行 app.ts 中的 getInitialState
await refresh();
history.push('/dashboard');
}
};
const handleThemeChange = (newTheme: string) => {
// 方式 2:手动合并修改状态
setInitialState((prev) => ({
...prev,
settings: { ...prev?.settings, theme: newTheme },
}));
};
return (
<div>
<button onClick={() => handleLogin({ user: 'admin', pass: '123' })}>登录</button>
<button onClick={() => handleThemeChange('dark')}>切换暗色模式</button>
</div>
);
}
由于 @@initialState 深度集成了 Umi 的 Access(权限守卫)与 Layout(通用中后台布局),配置完成后无需编写额外的 Context Provider,即可自动控制菜单显示、路由放行以及头像展示。
四、传统与重型场景:Dva 数据流的适用与实践
如果项目是从 Umi 2/3 演进而来,或者业务中充斥着复杂的异步时序处理(如长轮询联动、防抖节流异步流、多接口因果依赖),使用基于 Redux 与 Redux-Saga 的 Dva 插件依然是一种可选方案。
Dva 模型的基本结构
在开启 dva: {} 插件配置后,在 src/models 下创建符合 Dva 规范的模型对象:
// 示例文件位置:src/models/orderDva.ts
import { Effect, Reducer, Subscription } from 'umi';
import { fetchOrderList, cancelOrderApi } from '@/services/order';
export interface OrderModelState {
list: any[];
total: number;
}
export interface OrderModelType {
namespace: 'order';
state: OrderModelState;
effects: {
query: Effect;
cancel: Effect;
};
reducers: {
save: Reducer<OrderModelState>;
};
}
const OrderModel: OrderModelType = {
namespace: 'order',
state: {
list: [],
total: 0,
},
effects: {
*query({ payload }, { call, put }) {
const response = yield call(fetchOrderList, payload);
yield put({
type: 'save',
payload: response.data,
});
},
*cancel({ payload }, { call, put }) {
yield call(cancelOrderApi, payload.id);
// 取消成功后再次触发刷新
yield put({ type: 'query', payload: { page: 1 } });
},
},
reducers: {
save(state, action) {
return {
...state,
...action.payload,
};
},
},
};
export default OrderModel;
plugin-model。若无强烈的 Saga 异步逻辑需求,新项目尽量不要引入 Dva。Dva 的 action 字符串分发与 Generator 语法会增加初学者的认知负担与样板代码量。
五、数据流方案横向对比与关键避坑指南
在实际项目研发中,选对方案并规范使用方式,才能避免代码膨胀和性能退化。
| 方案 | 实现原理 | 典型使用场景 | 上手成本 | 性能控制手段 |
|---|---|---|---|---|
| plugin-model | React Custom Hook + Context 广播 | 用户身份、购物车、页面跨级通信、通用全局标记 | 低(原生 React Hooks) | useModel(key, selector) 字段级订阅 |
| @@initialState | 应用启动异步函数 + 预置 Model | 权限元数据、登录态、基础配置字典 | 低(框架内置) | 仅存放启动常量与登录基石,不存大列表 |
| Dva Plugin | Redux Store + Redux-Saga | 复杂业务工作流、连续异步串联、老项目重构 | 高(Action/Saga/Reducer) | useSelector 细粒度读取,避免全局 connect |
| 局部 useState | React 组件本地状态 | 表单字段、Modal 弹窗显隐、选项卡激活项 | 极低 | 状态下沉,随组件卸载自动销毁 |
常见陷阱与排查清单
-
避免“状态提升泛滥”:很多开发者习惯把每一个页面的表单临时输入、弹窗开关状态都写进
src/models。这不仅会污染全局命名空间,还会延长内存驻留周期。只有至少两个跨路由页面或深层级无关组件都需要共享的数据,才有资格进入全局 Model。 -
谨慎在非 React 上下文中调用状态:
useModel必须且只能在 React 函数组件内使用。如果你需要在纯 Axios 拦截器或工具函数中获取当前 Token,正确的做法是从localStorage读取,或者通过闭包导出引用的方式传递,切忌在普通 JS 函数中违规执行 Hook。 -
不要在 Model 内部执行无清理机制的长监听:如果在
plugin-model的 Hook 内部绑定了window.addEventListener或setInterval,务必使用useEffect并在返回函数中清理,避免内存泄漏。
React.js 新手入门:环境搭建与第一个组件开发
2026-11-03 19:40:05
React表单处理:控制与非控制组件实战指南
2026-11-01 04:11:20
JavaScript 使用 showdown 实现 markdown 预览功能
2026-10-31 11:28:47
前后端分离架构下,JavaScript与JSP的角色演变及替代方案
2026-10-30 18:41:20
ReactJS入门:Hello World组件化架构解析
2026-10-29 12:07:57








