状态管理
icejs 内置了状态管理方案,并在此基础上进一步遵循 “约定优于配置” 原则,进行抽象和封装,使得状态管理变得非常容易。
全局应用状态
定义 Model
约定全局状态位于 src/models
目录,目录结构如下:
src
├── models // 全局状态
| ├── counter.ts
│ └── user.ts
└── store.ts
假设我们需要全局管理用户状态,定义模型如下:
// src/models/user.ts
export const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time));
export default {
// 定义 model 的初始 state
state: {
name: '',
id: '',
},
// 定义改变该模型状态的纯函数
reducers: {
update(prevState, payload) {
return {
...prevState,
...payload,
};
},
},
// 定义处理该模型副作用的函数
effects: (dispatch) => ({
async getUserInfo() {
await delay(1000);
dispatch.user.update({
name: 'taobao',
id: '123',
});
},
}),
};
初始化 Store
// src/store.ts
import { createStore } from 'ice';
import user from './models/user';
import project from './models/project';
const store = createStore(
{
user,
project,
},
{
// options
},
);
export default store;
详细文档请参考 API-createStore 。
在 View 中使用模型状态
+ import store from '@/store';
const HomePage = () => {
+ const [userState, userDispatchers] = store.useModel('user');
return (
<>
<span>{userState.id}</span>
<span>{userState.name}</span>
</>
);
}
页面级状态
页面状态只能在该页面下的组件中使用,无法跨页面使用。
非嵌套页面
大部分场景中一个 pages/Foo
目录对应一个路由,即非嵌套页面,此时约定页面状态在 src/pages/*/models
中定义。
目录组织如下:
src
├── models // 全局状态
│ └── user.ts
└── pages
| ├── Home // Home 页面
+| │ ├── models // 页面状态
+| │ | ├── foo.ts
+| │ | └── bar.ts
+| │ ├── store.ts
| │ └── index.tsx
└── app.ts
定义模型如下:
// src/pages/Home/models/foo.ts
export default {
state: {
title: 'Hello',
},
};
初始化 Store 实例:
// src/pages/Home/store.ts
import { createStore } from 'ice';
import foo from './models/foo';
const store = createStore({ foo });
export default store;
在页面组件中使用模型状态:
// 引用页面状态
import pageStore from '@/pages/Home/store';
const HomePage = () => {
const [pageState, pageDispatchers] = pageStore.useModel('foo');
return (
<>{pageState.title}</> // Hello
);
};
嵌套页面
某些复杂场景会出现嵌套页面的情况,即 src/pages/Home
下包含多个路由页面,目录组织如下:
src
└── pages
│ ├── Home // Home 页面包含了 A、B 等多个路由页面
│ │ ├── HomeA
│ │ │ └── index.tsx
│ │ ├── HomeB
│ │ │ └── index.tsx
│ │ ├── Layout // 页面布局
│ │ │ └── index.tsx
│ │ ├── models // 页面状态
│ │ │ ├── Foo.ts
│ │ │ └── Bar.ts
│ │ └── store.ts
└── app.ts
对于嵌套页面,框架会将 store 的 Provider 包裹在 Layout/index.tsx
上,此时所有嵌套的页面以及组件都可以访问到这里的 store。Layout/index.tsx
内容如下:
// Layout 中可以定义这些嵌套页面共用的布局,如果没有共用布局则直接渲染 children 即可
export default ({ children }) => {
return <>{children}</>;
};
同时配置在 src/routes.ts
中:
// src/routes.ts
+import HomeLayout from '@/pages/Home/Layout';
+import HomeA from '@/pages/Home/HomeA';
+import HomeB from '@/pages/Home/HomeB';
import About from '@/pages/About';
export default [
{
path: '/',
component: BasicLayout,
children: [
{
path: '/home',
+ component: HomeLayout,
+ children: [
+ {
+ path: '/a',
+ component: HomeA
+ }
+ ]
},
{
path: '/about',
component: About,
}
]
}
]
高阶用法
设置初始状态
假设我们有 models/user.ts
和 models/counter.ts
两个模型,可以通过 runApp()
中的 store.initialStates
设置初始状态:
runApp({
app: {},
store: {
// 可选,初始化状态
initialStates: {
user: { name: 'foo' },
counter: { count: 0 }
},
},
});
如果初始的状态数据需要异步获取,则需要结合 app.getInitialData()
实现:
runApp({
app: {
getInitialData: async (ctx) => {
const { username, count } = await request.get('/api/data');
return {
// initialStates 是约定好的字段,会透传给 store 的初始状态
initialStates: {
user: { name: username },
counter: { count }
}
}
}
},
});
注意:页面级状态目前不支持设置 initialStates
TypeScript 类型提示
编写类型有助于更好的代码提示,类型定义步骤如下:
- 创建 Store 实例
// src/store.ts
import { createStore, IStoreModels, IStoreDispatch, IStoreRootState } from 'ice';
import user from './models/user';
import porject from './models/porject';
+interface IAppStoreModels extends IStoreModels {
+ user: typeof user;
+ project: typeof project;
+};
+const appModels: IAppStoreModels = {
+ user,
+ project,
+};
export default createStore(appModels);
+export type IRootDispatch = IStoreDispatch<typeof appModels>;
+export type IRootState = IStoreRootState<typeof appModels>;
- 定义状态模型
// src/models/user.ts
+import { IRootState, IRootDispatch } from '@/store';
const user = {
state: [],
reducers: {},
+ effects: ((dispatch: IRootDispatch) => ({
+ like(payload, rootState: IRootState) {
+ dispatch.project.foo(payload); // 调用其他 model 的 effects/reducers
+ rootState.project.title; // 获取其他 model 的 state
+ }
+ })
};
Model 定义详细说明
如上示例所述,icejs 约定在 src/models
、src/pages/*/models
目录下的文件为项目定义的模型文件,每个文件需要默认导出一个对象。
通常模型定义包括 state、reducers、effects 三部分:
export default {
state: {},
reducers: {},
effects: {},
};
state
model 的初始 state
export default {
+ state: { count: 0 }
}
reducers
reducers: { [string]: (prevState, payload) => any }
一个改变该模型状态的函数集合。这些方法以模型的上一次 prevState 和一个 payload 作为入参,在方法中使用可变的方式来更新状态。这些方法应该是仅依赖于 prevState 和 payload 参数来计算下一个 nextState 的纯函数。对于有副作用的函数,请使用 effects。
export default {
state: { count: 0, list: [] },
+ reducers: {
+ increment (prevState, payload) {
+ const newList = prevState.list.slice();
+ newList.push(payload);
+ const newCount = prevState.count + 1;
+ return { ...prevState, count: newCount, list: newList }
+ },
+ decrement (prevState) {
+ return { ...prevState, count: prevState.count - 1 }
+ }
+ }
}
effects
effects: (dispatch) => ({ [string]: (payload, rootState) => void })
一个可以处理该模型副作用的函数集合。这些方法以 payload 和 rootState 作为入参,适用于进行异步调用、模型联动等场景。
export default {
state: { count: 0 },
reducers: {
increment (prevState) {
return {
...prevState,
count: prevState.count + 1
}
},
decrement (prevState) {
return {
...prevState,
count: prevState.count - 1
}
}
},
+ effects: (dispatch) => ({
+ async asyncDecrement() {
+ await delay(1000); // 进行一些异步操作
+ this.increment(); // 调用模型 reducers 内的方法来更新状态
+ },
+ }),
};
Model 之间通信
注意:如果两个 model 不属于同一个 store 实例,是无法通信的
// src/models/user
export default {
state: {
name: '',
tasks: 0,
},
effects: () => ({
async refresh() {
const data = await fetch('/user');
+ // 通过 this.foo 调用自身的 reducer
+ this.setState(data);
},
}),
};
// src/models/tasks
export default {
state: [],
effects: (dispatch) => ({
async refresh() {
const data = await fetch('/tasks');
this.setState(data);
},
async add(task) {
await fetch('/tasks/add', task);
+ // 调用另一个 model user 的 effects
+ await dispatch.user.refresh();
+ // 通过 this.foo 调用自身的 effects
+ await this.refresh();
},
}),
};
在 effects 里的 action 方法中可以通过 dispatch[model][action]
拿到其他模型所定义的方法。
如果遇到
this.foo
的 ts 类型错误,请参考文档 icestore QA 进行修复setState 是 icestore 内置的一个 reducer,可以直接使用
Model 中使用 immer 更改 state
Redux 默认的函数式写法在处理一些复杂对象的 state 时会非常繁琐,因此 icejs 同时支持了使用 immer 来操作 state:
export default {
state: {
tasks: ['A Task', 'B Task'],
detail: {
name: 'Bob',
age: 3,
},
},
reducers: {
addTasks(prevState, payload) {
- return {
- ..prevState,
- tasks: [ ...prevState.tasks, payload ],
- },
+ prevState.tasks.push(payload);
},
updateAge(prevState, payload) {
- return {
- ..prevState,
- detail: {
- ...prevState.detail,
- age: payload,
- },
- },
+ prevState.detail.age = payload
}
}
}
注意:因为 immer 无法支持字符串或数字这样的简单类型,因此如果 state 符合这种情况(极少数)则不支持通过 immer 操作,必须使用 Redux 默认的函数式写法(返回一个新值):
const count = {
state: 0,
reducers: {
add(prevState) {
- state += 1;
+ return state += 1;
},
},
}
获取 effects 的状态 loading/error
通过 useModelEffectsState
API 即可获取到 effects 的 loading 和 error 状态。
import store from '@/store';
function FunctionComponent() {
const [state, dispatchers] = store.useModel('counter');
+ const effectsState = store.useModelEffectsState('counter');
useEffect(() => {
dispatchers.asyncDecrement();
}, []);
+ console.log(effectsState.asyncDecrement.isLoading); // loading
+ console.log(effectsState.asyncDecrement.error); // error
}
在 Class Component 中使用
useModel 相关的 API 基于 React 的 Hooks 能力,仅能在 Function Component 中使用,通过 withModel
API 可以实现在 Class Component 中使用。
import store from '@/store';
class TodoList extends React.Component {
render() {
const { todos } = this.props;
const [state, dispatchers] = todos;
// ...
}
}
export default store.withModel('todos')(TodoList);
// 绑定多个 model
// export default withModel('user')(withModel('todos')(TodoList));
同时,也可以使用 withModelDispatchers
以及 withModelEffectsState
API。
Redux Devtools
icejs 中默认集成了 Redux Devtools,不需要额外的配置就可以通过 Redux Devtools 调试:
如果想要定义 Devtools 的参数,可以查看上面 createStore
的 options 说明。
在其他地方使用 store
满足以下几种情况,框架都会自动帮助开发者包裹 store.Provider
:
- SPA 全局 store:
src/
下有store.ts
和app.ts
- SPA 页面级 store:
src/pages/Home
下有store.ts
和index.tsx
- SPA 嵌套页面级 store:
src/pages/Home
下有store.ts
和Layout/index.tsx
(优先级低于上面) - MPA 组件类型的 entry:
src/pages/Home
下有store.ts
和index.tsx
- MPA 单页类型的 entry:
src/pages/Home
下有store.ts
,app.ts
,Layout/index.tsx
如果不满足上述情况,则需要开发者自行包裹 store.Provider
。比如希望在 src/pages/Home/Foo/
下创建一个 store:
- 在
src/pages/Home/Foo/models/
下定义 model - 在
src/pages/Home/Foo/store.ts
中初始化 store - 新增步骤: 在
src/pages/Home/Foo/index.tsx
中包裹store.Provider
// src/pages/Home/Foo/index.tsx
import store from './store';
const { Provider } = store;
export default () => {
return (
<Provider>
<Child />
</Provider>
);
};
function Child() {
const [state, actions] = store.useModel('foo');
return <></>;
}
使用其他状态管理方案
icejs 默认使用 @ice/store 作为状态管理方案,如需使用其他方案,需要在 build.json 中通过选项关闭默认方案:
{
"store": false
}
此时项目不会再引入 @ice/store
相关的各种能力,包含上述的自动包裹 Provider
等,此时就可以灵活的引入其他状态管理方案了。
版本变更说明
内置的 immer 从 6.x 升级到最新版本 9.x
icejs 2.0.0 版本升级
不再自动初始化 store
1.9.7 版本标记废弃,2.0.0 版本完全移除
推荐开发者自行创建 store.ts
并在其中初始化 store,这样可以更灵活的定制一些参数,相对之前方案带来的改变:
- 开发者需要在
models/
同层目录自行创建store.ts
并初始化 store 实例:
src
└── models
| ├── foo.ts
| └── bar.ts
+|── store.ts
└── app.ts
// src/store.ts
import { createStore } from 'ice';
import user from './models/user';
import project from './models/project';
const store = createStore({
user,
project,
});
export default store;
- 引入 store 的路径发生了变化:
// 全局状态
- import { store } from 'ice';
+ import store from '@/store';
// 页面级状态
- import { store } from 'ice/Home';
+ import store from '@/pages/Home/store';
Model 中不再支持 actions: {}
写法
1.7.0 版本标记废弃,2.0.0 版本完全移除
将原先的 actions: {}
拆分为 effects: () => {}
和 reducers: {}
两个字段:
const counter = {
state: { value: 0 },
- actions: {
- increment:(state) => ({ value: state.value + 1 }),
- async asyncIncrement(state, payload, actions, globalActions) {},
- }
+ reducers: {
+ increment:(prevState) => ({ value: prevState.value + 1 }),
+ },
+ effects: (dispatch) => ({
+ async asyncIncrement(payload, rootState) {},
+ }),
}
不再支持 store.getInitialStates()
1.7.0 版本标记废弃,2.0.0 版本完全移除
推荐使用 store.initialStates
。
路由切换后重新初始化页面状态
icejs 1.0 中有一个「错误」的设计,切换页面再次进入原页面后页面状态不会重新初始化,如需重新初始化需要主动配置:
{
"store": {
"resetPageState": true
}
}
icejs 2.0 版本将此默认行为进行了修正,切换页面再次进入原页面后会重新初始化页面状态,如果希望跟 1.0 表现一致,则需要主动配置:
{
"store": {
"disableResetPageState": true
}
}