ice/website/docs/guide/advanced/store.md

533 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 调试:
![](https://img.alicdn.com/tfs/TB1wK4nqypE_u4jSZKbXXbCUVXa-1918-430.png)
如果需要定义 Devtools 的参数,可以在 `createStore` 的 options 入参中配置:
```ts
createStore({ user }, {
redux: {
devtoolOptions: {
// 更多配置参考https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md
}
}
})
```