mirror of https://github.com/alibaba/ice.git
533 lines
11 KiB
Markdown
533 lines
11 KiB
Markdown
---
|
||
title: 状态管理
|
||
order: 0204
|
||
---
|
||
|
||
import Tabs from '@theme/Tabs';
|
||
import TabItem from '@theme/TabItem';
|
||
|
||
<details open>
|
||
<summary>示例</summary>
|
||
<ul>
|
||
<li>
|
||
<a href="https://github.com/alibaba/ice/tree/master/examples/with-store" target="_blank" rel="noopener noreferrer">
|
||
with-store
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</details>
|
||
|
||
ice.js 基于 [icestore](https://github.com/ice-lab/icestore) ,提供主流的状态管理解决方案,以更好管理复杂的状态管理逻辑。
|
||
|
||
## 开启状态管理
|
||
|
||
安装插件:
|
||
|
||
```bash
|
||
$ npm i @ice/plugin-store -D
|
||
```
|
||
|
||
在 `ice.config.mts` 中添加插件:
|
||
|
||
```ts title="ice.config.mts"
|
||
import { defineConfig } from '@ice/app';
|
||
import store from '@ice/plugin-store';
|
||
|
||
export default defineConfig(() => ({
|
||
plugins: [
|
||
store(),
|
||
],
|
||
}));
|
||
```
|
||
|
||
## 全局状态
|
||
|
||
推荐在不同页面组件中共享的状态存放在全局状态中,比如主题、国际化语言、用户信息等。
|
||
|
||
### 定义 Model
|
||
|
||
约定在 `src/models` 目录定义全局状态。以定义全局用户状态为例:
|
||
|
||
```ts title="src/models/user.ts"
|
||
import { createModel } from 'ice';
|
||
|
||
interface User {
|
||
name: string;
|
||
id: string;
|
||
}
|
||
|
||
export default createModel({
|
||
// 定义 model 的初始 state
|
||
state: {
|
||
name: '',
|
||
id: '',
|
||
} as User,
|
||
// 定义改变该 model 状态的纯函数
|
||
reducers: {
|
||
update(state, payload) {
|
||
return {
|
||
...state,
|
||
...payload,
|
||
};
|
||
},
|
||
},
|
||
// 定义处理该 model 副作用的函数
|
||
effects: (dispatch) => ({
|
||
async getUserInfo() {
|
||
await delay(1000);
|
||
this.update({
|
||
name: 'taobao',
|
||
id: '123',
|
||
});
|
||
},
|
||
}),
|
||
})
|
||
```
|
||
|
||
### 初始化 Store
|
||
|
||
约定在 `src/store.ts` 中初始化 Store:
|
||
|
||
```ts title="src/store.ts"
|
||
import { createStore } from 'ice';
|
||
import user from './models/user';
|
||
|
||
export default createStore({ user });
|
||
```
|
||
|
||
### 在组件中使用
|
||
|
||
```diff
|
||
import { useEffect } from 'react';
|
||
+ import store from '@/store';
|
||
|
||
export default function Home() {
|
||
+ const [userState, userDispatchers] = store.useModel('user');
|
||
|
||
+ useEffect(() => {
|
||
+ // 触发 dispatcher 获取数据并修改 state
|
||
+ userDispatchers.getUserInfo()
|
||
+ }, [])
|
||
return (
|
||
<>
|
||
+ <span>{userState.id}</span>
|
||
+ <span>{userState.name}</span>
|
||
</>
|
||
);
|
||
}
|
||
```
|
||
|
||
## 页面状态
|
||
|
||
:::caution
|
||
|
||
页面状态只能在该页面下的组件中使用,无法跨页面使用。
|
||
:::
|
||
|
||
### 定义 Model
|
||
|
||
约定在当前路由目录下新建 models 目录并定义 Model:
|
||
|
||
```diff
|
||
src
|
||
└── pages
|
||
| ├── home // /home 页面
|
||
+| │ ├── models // 定义 model
|
||
+| │ | └── info.ts
|
||
| │ └── index.tsx
|
||
```
|
||
|
||
定义 Model 如下:
|
||
```ts title="src/pages/home/models/info.ts"
|
||
import { createModel } from 'ice';
|
||
|
||
export default createModel({
|
||
state: {
|
||
title: '',
|
||
},
|
||
reducers: {
|
||
update(state, payload) {
|
||
return {
|
||
...state,
|
||
...payload,
|
||
};
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
### 初始化 Store
|
||
|
||
约定在当前路由目录下新建 store 文件:
|
||
|
||
```diff
|
||
src
|
||
└── pages
|
||
| ├── home // /home 页面
|
||
| │ ├── models // 定义 model
|
||
| │ | └── info.ts
|
||
+| │ ├── store.ts // 创建 store
|
||
| │ └── index.tsx
|
||
```
|
||
|
||
```ts title="src/pages/home/store.ts"
|
||
import { createStore } from 'ice';
|
||
import info from './models/info';
|
||
|
||
const store = createStore({ info });
|
||
|
||
export default store;
|
||
```
|
||
|
||
### 在组件中使用
|
||
|
||
```diff title="src/pages/home/index.tsx"
|
||
import { useEffect } from 'react';
|
||
+ import homeStore from './store';
|
||
|
||
export default function Home() {
|
||
+ const [infoState, infoDispatchers] = homeStore.useModel('info');
|
||
|
||
+ useEffect(() => {
|
||
+ infoDispatchers.update({ title: 'ICE' })
|
||
+ }, [])
|
||
return (
|
||
+ <h1>{infoState.title}</h1>
|
||
);
|
||
};
|
||
```
|
||
|
||
## 进阶用法
|
||
|
||
### 设置初始状态
|
||
|
||
:::caution
|
||
|
||
页面级状态目前不支持设置 `initialStates`。
|
||
|
||
:::
|
||
|
||
假设我们有 `user` 和 `counter` 两个 Model:
|
||
|
||
<Tabs>
|
||
<TabItem value="store" label="src/store.ts">
|
||
|
||
```ts
|
||
import { createStore } from 'ice';
|
||
import user from './models/user';
|
||
import counter from './models/counter';
|
||
|
||
export default createStore({ user, counter });
|
||
```
|
||
|
||
</TabItem>
|
||
<TabItem value="user" label="src/models/user.ts">
|
||
|
||
```ts
|
||
import { createModel } from 'ice';
|
||
|
||
export default createModel({
|
||
state: {
|
||
name: '',
|
||
}
|
||
});
|
||
```
|
||
|
||
</TabItem>
|
||
<TabItem value="counter" label="src/models/counter.ts">
|
||
|
||
```ts
|
||
import { createModel } from 'ice';
|
||
|
||
export default createModel({
|
||
state: {
|
||
count: 0,
|
||
}
|
||
});
|
||
```
|
||
|
||
</TabItem>
|
||
</Tabs>
|
||
|
||
我们可以在 `src/app.ts` 中设置两个 Model 初始状态:
|
||
|
||
```ts title="src/app.ts"
|
||
import { defineStoreConfig } from '@ice/plugin-store/types';
|
||
|
||
export const storeConfig = defineStoreConfig(async () => {
|
||
// 模拟请求后端数据
|
||
// const data = (await fetch('your-url')).json();
|
||
return {
|
||
initialStates: {
|
||
// initialStates 键值与 createStore 的第一个入参键值保持一致
|
||
user: {
|
||
name: 'ice.js',
|
||
},
|
||
counter: {
|
||
count: 1
|
||
}
|
||
},
|
||
};
|
||
});
|
||
```
|
||
|
||
### Model 定义详细说明
|
||
|
||
插件约定在 `src/models`、`src/pages/**/models` 目录下的文件为项目定义的 model 文件,每个文件需要默认导出一个对象。
|
||
|
||
|
||
#### state
|
||
|
||
定义 Model 的初始 state:
|
||
```ts
|
||
import { createModel } from 'ice';
|
||
|
||
export default createModel({
|
||
state: { count: 0 },
|
||
})
|
||
```
|
||
|
||
#### reducers
|
||
|
||
```ts
|
||
type Reducers = {
|
||
[k: string]: (state, payload) => any;
|
||
};
|
||
```
|
||
|
||
一个改变该模型状态的函数集合。这些方法以模型的上一次 `state` 和一个 `payload`(调用 reducer 时传入的参数)作为入参,在方法中使用可变的方式来更新状态。 这些方法应该是仅依赖于 `state` 和 `payload` 参数来计算下一个 `state` 的纯函数。对于有副作用的函数,请使用 [`effects`](#effects) 。
|
||
|
||
```ts
|
||
import { createModel } from 'ice';
|
||
|
||
export default ({
|
||
state: { count: 0, list: [] },
|
||
|
||
reducers: {
|
||
increment (state, payload) {
|
||
const newList = state.list.slice();
|
||
newList.push(payload);
|
||
const newCount = state.count + 1;
|
||
return { ...state, count: newCount, list: newList }
|
||
},
|
||
decrement (state) {
|
||
return { ...state, count: state.count - 1 }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### effects
|
||
|
||
```ts
|
||
type Effects = (dispatch) => ({ [string]: (payload, rootState) => void })
|
||
```
|
||
|
||
一个可以处理该模型副作用的函数集合。这些方法以 `payload` 和 `rootState`(当前模型的 state) 作为入参,适用于进行异步调用、模型联动等场景。
|
||
|
||
|
||
```ts
|
||
import { createModel } from 'ice';
|
||
|
||
export default createModel({
|
||
reducers: {
|
||
increment() {
|
||
// ...
|
||
}
|
||
},
|
||
effects: (dispatch) => ({
|
||
async asyncDecrement() {
|
||
const list = (await fetch('your-url')).json(); // 进行一些异步操作
|
||
this.increment(list); // 调用模型 reducers 内的方法来更新状态
|
||
},
|
||
}),
|
||
})
|
||
```
|
||
|
||
### Model 之间通信
|
||
|
||
:::caution
|
||
|
||
如果两个 Model 不属于同一个 Store 实例,是无法通信的
|
||
|
||
:::
|
||
|
||
<Tabs>
|
||
<TabItem value="user" label="src/models/user.ts">
|
||
|
||
```ts
|
||
// src/models/user.ts
|
||
import { createModel } from 'ice';
|
||
|
||
export default createModel({
|
||
state: {
|
||
name: '',
|
||
tasks: 0,
|
||
},
|
||
effects: () => ({
|
||
async refresh() {
|
||
const data = (await fetch('/user')).json();
|
||
// 通过 this.foo 调用自身的 reducer
|
||
this.setState(data);
|
||
},
|
||
}),
|
||
});
|
||
```
|
||
</TabItem>
|
||
<TabItem value="task" label="src/models/tasks.ts">
|
||
|
||
```ts
|
||
// src/models/tasks.ts
|
||
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();
|
||
},
|
||
}),
|
||
};
|
||
```
|
||
|
||
</TabItem>
|
||
</Tabs>
|
||
|
||
|
||
### 使用不可变状态
|
||
|
||
Redux 默认的函数式写法在处理一些复杂对象的 state 时会非常繁琐。推荐使用 [immer](https://immerjs.github.io/immer/) 的方式来操作 state:
|
||
|
||
```diff
|
||
import { createModel } from 'ice';
|
||
|
||
export default createModel({
|
||
state: {
|
||
tasks: ['A Task', 'B Task'],
|
||
detail: {
|
||
name: 'Bob',
|
||
age: 3,
|
||
},
|
||
},
|
||
reducers: {
|
||
addTasks(state, payload) {
|
||
- return {
|
||
- ...state,
|
||
- tasks: [ ...state.tasks, payload ],
|
||
- },
|
||
+ state.tasks.push(payload);
|
||
},
|
||
updateAge(state, payload) {
|
||
- return {
|
||
- ...state,
|
||
- detail: {
|
||
- ...state.detail,
|
||
- age: payload,
|
||
- },
|
||
- },
|
||
+ state.detail.age = payload;
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
注意:因为 immer 无法支持字符串或数字这样的简单类型,因此如果 state 符合这种情况(极少数)则不支持通过 immer 操作,必须使用 Redux 默认的函数式写法(返回一个新值):
|
||
|
||
```diff
|
||
import { createModel } from 'ice';
|
||
|
||
export default createModel({
|
||
state: 0,
|
||
reducers: {
|
||
add(state) {
|
||
- state += 1;
|
||
+ return state += 1;
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
### 获取内置的加载状态和错误状态
|
||
|
||
通过 `useModelEffectsState` API 即可获取到 `effects` 的 加载状态( `isLoading` )和 错误状态(`error`)。
|
||
|
||
```diff
|
||
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); // true
|
||
+ console.log(effectsState.asyncDecrement.error); // null
|
||
}
|
||
```
|
||
|
||
### 页面切换后重置状态
|
||
|
||
在单页应用下进行页面切换时,页面状态是会保留的。如果想切换页面后再次进入原页面时重新初始化页面状态,需要添加以下配置:
|
||
|
||
```diff title="ice.config.mts"
|
||
import { defineConfig } from '@ice/app';
|
||
import store from '@ice/plugin-store';
|
||
|
||
export default defineConfig(() => ({
|
||
plugins: [
|
||
- store(),
|
||
+ store({ resetPageState: true }),
|
||
],
|
||
}));
|
||
```
|
||
|
||
### 在 Class Component 中使用
|
||
|
||
通过 `withModel` 可以实现在 Class Component 中使用状态管理。
|
||
|
||
```tsx
|
||
import store from '@/store';
|
||
|
||
@store.withModel('todos')
|
||
export default class TodoList extends React.Component {
|
||
render() {
|
||
const { todos } = this.props;
|
||
const [state, dispatchers] = todos;
|
||
console.log('state: ', state);
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
:::tip
|
||
|
||
TS 应用需要在 `tsconfig.json` 里添加 `compilerOptions: { "experimentalDecorators": true }`
|
||
才可启用装饰器语法。
|
||
:::
|
||
|
||
### Redux Devtools
|
||
|
||
插件中默认集成了 Redux Devtools,不需要额外的配置就可以在 Redux Devtools 调试:
|
||
|
||

|
||
|
||
如果需要定义 Devtools 的参数,可以在 `createStore` 的 options 入参中配置:
|
||
|
||
```ts
|
||
createStore({ user }, {
|
||
redux: {
|
||
devtoolOptions: {
|
||
// 更多配置参考:https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md
|
||
}
|
||
}
|
||
})
|
||
```
|