Umi 小白纪实(四)—— 在 Umi 3.x 中使用 dva
公司的新项目用到了 Umi,之前用过 Umi 1.x 版本,而现在已经来到了 3.x 时代
相对低版本来说,Umi 3.x 的整体的设计没有什么大变化,但在细节上还是有着不小的改变
比如 model,除了兼容以前的 connect 写法之外,还可以使用 hooks,这篇文章主要是对 hooks 写法的介绍
// Umi 3.x 默认使用 ts,但为了便于理解,文中的代码都是用的 js,可以自行调整为 ts
一、model 用例
model 的结构和之前没有变化,依然是包含以下几部分
export default {
namespace: '', // 表示在全局 state 上的 key
state: {}, // 状态数据
reducers: {}, // 管理同步方法,必须是纯函数
effects: {}, // 管理异步操作,采用了 generator 的相关概念
subscriptions: {}, // 订阅数据源
};
详细介绍可以参考之前的文章《Umi 小白纪实(二)—— model 的注册与使用》
这里先写一个简单的 testModel,后面会基于此来介绍 dva 的 hooks
const testModel = {
namespace: 'testModel',
state: {
count: 0,
},
reducers: {
change: state => {
state.count++;
return state;
},
},
}
export default testModel;
二、老办法 connect
如果是用 connect 写法,需要用 dva 或 umi 中导出 connect 方法,然后将 model 绑定到组件
// test-page
import { connect } from 'umi';
import styles from './index.module.less';
function TestPage(props) {
// title 在 testModel 中定义, dispatch 由 dva 提供
const { count, dispatch } = props;
const handleClick = () => {
// 使用 dispatch 调用 testModel 中的 change 方法
dispatch({ type: "testModel/change" })
}
return (
<div className={styles.testPage}>
<h1>Hello World</h1>
<h3>{count}</h3>
<button onClick={handleClick}>Update Count</button>
</div>
);
}
// 这里的 testModel 是对应 model 的 namespace
function mapStateToProps({ testModel }) {
return { ...testModel };
};
// 使用 connect 将数据关联到组件
export default connect(mapStateToProps)(TestPage);
这种方式在 Umi 3.x 中依然可以使用,不过既然用上了最新的 React 和 Umi,为什么不尝试一下 hooks 呢?
三、新思路 hooks
dva 2.6x 之后,就提供了 useSelector、useDispatch 这两个 hook
如果用过 redux,对它们应该不算陌生,而事实上 dva 确实是基于 redux 实现,所以这些 api 的用法和 redux 中没什么区别
这种思路不会引用 connect,也就不会将 model 中的数据直接关联到组件中,但可以通过 useSelector 拿到 model 中的 state
而且通过 useSelector 获取的 state 会建立发布订阅模式,当 model 中的 state 变化的时候,会再次执行 useSelector 以更新组件中的 state
// before
connect(Model, Component);
const { count, dispatch } = props;
// after
const dispatch = useDispatch();
const count = useSelector(state => state.testModel.count);
所以上面使用 connect 的组件,在使用 hooks 之后可以这么来:
// test-page
import { useSelector, useDispatch } from 'umi';
import styles from './index.module.less';
function TestPage(props) {
// 通过 hook 获取 dispatch 和 count
const dispatch = useDispatch();
const count = useSelector(state => state.testModel.count);
const handleClick = () => {
// 使用 dispatch 调用 testModel 中的 change 方法
dispatch({ type: "testModel/change" })
}
return (
<div className={styles.testPage}>
<h1>Hello World</h1>
<h3>{count}</h3>
<button onClick={handleClick}>Update Count</button>
</div>
);
}
export default TestPage;
四、新问题:路由切换后 state 没有重置
先来跑一下测试页面吧:
可以看到,在跳转回首页之后,测试页面 model 的 state 并没有重置
这个问题本身倒不难解决,在组件销毁的时候重置 state 就可以
// model.js
const getDefaultState = () => ({
title: 'there is test page',
})
const testModel = {
namespace: 'testModel',
state: getDefaultState(),
reducers: {
// 创建重置 state 的方法
reset: getDefaultState,
},
}
export default testModel;
但在纯函数组件中,怎么判断销毁呢?
关于这一点可以看一下 useEffect 的官方文档:如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它
但仅仅如此还不够,因为这个清除操作并不是销毁组件,我们需要使用 useEffect 的第二个参数
import { useEffect } from 'react';
function TestPage(props) {
// ...
useEffect(() => {
// 调用 model 中的 reset 方法重置
return dispatch({ type: "testModel/reset" });
// 传入空数组,返回的函数会在组件销毁时执行
}, []);
// ...
}
export default TestPage;
给 useEffect 传入一个空数组,就能让它只在渲染完成后加载,并在组件销毁时执行其返回的函数,类似于 componentDidMount 和 componentWillUnmount
当然这里也可以通过路由地址来判断:
import { useLocation } from 'umi';
function TestPage(props) {
// ...
const location = useLocation();
useEffect(() => {
// ...
}, [location.pathname]);
// ...
}
这样该 hook 就只会在路由地址发生变化时执行