React Hook 系列(三):记一次中台项目的Hook沉淀
React Hook 系列(三):记一次中台项目的Hook沉淀
背景
本文旨在分享,React hook
在中大型中台项目中的实践,适合熟悉 React hook
用法的同学,希望能对你有帮助。
用到的库
1. unstated-next
200 bytes to never think about React state management libraries ever again.
永远不必再考虑 React 状态管理了,仅仅 200 字节的状态管理解决方案。
unstated-next 主要是利用 React.createContext
状态共享,将需要注入 Provider
的状态以及状态更新操作抽象到 hook
中,提供给 Function Component
用的一个状态管理库。ts源码只有 40 行。
源码分析
export function createContainer(useHook) {
let Context = React.createContext<Value | typeof EMPTY>(EMPTY);
function Provider(props) {
let value = useHook(props.initialState);
// 将 hook 返回值 暴露给 Provider 的 value
return <Context.Provider value={value}>{props.children}</Context.Provider>;
}
function useContainer() {
// 用 useContext 获取 Context 上传递的 value
let value = React.useContext(Context);
if (value === EMPTY) {
throw new Error("Component must be wrapped with <Container.Provider>");
}
return value;
}
return { Provider, useContainer };
}
复制代码
2. use-immer
A hook to use immer as a React hook to manipulate state.
一个用于将 immer 作为 React hook 来操纵状态的 hook。
use-immer
可以将 state 数据 immutable
,更新深层嵌套数据更为方便,且有函数编程的感觉。
const [value, setValue] = useImmer({
a: { b: { c: { d: 12 } }, b2: { c: 34 } },
});
// 某些场景下我只需改变的的值
setValue((draft) => {
draft.a.b.c.d = 19;
});
// 数组某个值的变化
setValue((draft) => {
draft[2].name= 19;
});
复制代码
3. sunflower-antd
一些流程组件的自定义 hook,例如useModal
, useModalForm
等,提升效率明显。 源码
项目实践
1. 烦不胜烦的 modal
在中台项目中,对一些列表的资源信息CRUD 弹窗是必不可少的,所以页面中table的管理必不可少,且很繁琐,容易混乱。期初我是这样
<ConfigModal ... />
<EditModal ... />
<RenameModal ... />
复制代码
很麻烦,且重复搬砖的代码很多。最终我们用了 context 和 useHook 全局挂载激活的方式将 modal 和每个列表页解耦。 下面我们逐步分析如何优雅的写modal。
Modal
的一次生命周期基本包括:
特点:
modal
的打开和关闭由用户操作决定。- 需要记录每次选中的数据,传给要操作的
modal
。 - 点击提交成功后都需要关闭
modal
和页面触发刷新的操作。
每次只有一个激活的modal
和选中的数据一一对应,两者都是用操作的一瞬间确定的,且每次只有一个 modal 处于激活状态,所以用户的各种操作只是不断更新modal
和data
而已,所以假如全局有一个的专门记录 modal 的地方,这样我们只需将用户要激活的modal
不断替换,然后在全局的某处挂载当前激活的modal
。在使用modal
的页面中,我们只需不断去更新全局记录值,当modal
关闭时只需全局记录值置为空即可。这样在当前的页面中不需要再将烦人的众多modal
一次次的引入,也不需要维护一系列的visible
。问题来了,那如全局记录值modal
呢? 聪明的你可能已经想到了context
,没错就是它:
全局 Modal Context 记录的当前激活的 modal
const ModalContainer = createContainer(() => {
const [modal, setModal] = useState<ReactNode>(null);
return [modal, setModal] as const;
});
复制代码
指定全局挂载modal
的节点
const [modal] = ModalContainer.useContainer();
<div id="active-modal">{modal}</div>;
复制代码
激活当前要操作的 modal,我们自定义了useAction
, 它的作用就是返回一匿名函数,他有两个参数key、 data
,Key 和 modal 一一对应,data表示当前操作行的数据。最后将data传入通过key确定的modal中,塞进全局的modalContext中。
// fn 是一个根据key返回对应 modal 的函数。
export function useAction(fn) {
const [, setModal] = ModalContainer.useContainer();
const fnRef = useRef(fn);
// ...
// key 标识对应的 modal, data为当前操作数据
return (key, data) => {
// 根据key确定返回正在操作的modal。
const Result = fnRef.current(key, data);
// 将 modal set进 context 里,就会激活modal显示。
setModal(<Result data={data} />);
};
}
复制代码
接下来只需要,将useAction暴露给用户执行,通过传入的key和modal对应关系确定即将操作的modal,所以需要一一列举,他们的对应关系,我们自定义了 useActionCallback
, 它接收列举所有 modal
的回调函数 fn,fn 根据传入的参数确定具体的 modal。
export function useActionCallback() {
// 返回上面匿名函数 (key, data) => setModal(<Result data={data} />);
return useAction((key, data) => {
switch (key) {
case Operations.Create:
return CreateModal;
// ...
default:
return null;
})
}
复制代码
最终,在页面激活modal
只需要如下调用即可:
const onAction = useActionCallback();
<Button type="primary" onClick={() => onAction(Operations.Edit, data)}>
编辑
</Button>;
复制代码
至此,context
和 hook
让页面和 modal
解耦,它们的联系只有 data
, 而 data
又作为参数随时可以传入。 这优雅的写法,是不是让你耳目一新,心动了。
最后再看下modal
的内幕,modal
的 visible
参数默认是true
,当setModal
后挂载,它才会被弹出显示。当 modal 关闭时,需要将全局挂载的 modal
置空,所以把全局ModalContainer
记录的modal
置空即可。
// useActionModal 自定义hook 主要获取 modalProps
// modal关闭事件中置空
afterClose() {
// ...
setModal(null);
},
// ...
复制代码
2. 高度一致化的 table
在中台项目中,table 列表是很多模块的首页,基本包含 table顶部输入框搜索
、table列搜索和筛选
、分页
、行右键操作
和 自定义列
等。 每个列表页面的不同点:列定义、数据、数据来源,其他的内容基本复制粘贴一把梭。如何解决这中无脑搬砖的活异常重要。 最终我们层层封装,将数据和操作暴露出来,其余搬砖部分通过 useHook 和 Context 全部封装。 下面分析一下我们的解法。
table顶部输入框搜索
table顶部输入框搜索
输入框的内容和列表数据的来源相关。 需求: 在跳转详情后返回,输入框的内容需要保持跳转前一样且执行查询数据,所以记录这个输入框的内容是关键。 解决方法: history state
、 localStorage
、页面公共部分隐藏域
和 URL queryString
。
显而易见 URL
记录更容易分享、收藏和更直观的展示。最终 table顶部输入框搜索的关键字 被记录在 url querystring
中,只需要观察监听 URL的querystring变化即可,这里用到了 react-router
的 useLocation
即可监听。
那么问题来了,useLocation
是如何监听 url
的变化的?
阅读源码可知,react-router
也是通过 context
将 location history
统一管理及传递。然后通过监听浏览器 history
的 popstate
事件来触发更新 context
的 location
。
Router
组件部分源码如下:
this.unlisten = props.history.listen(location => {
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
复制代码
history.listen
来自于 history
库,如下:
const PopStateEventType = 'popstate';
function handlePop() {
// ...
}
window.addEventListener(PopStateEventType, handlePop);
let history: MemoryHistory = {
...
listen(listener) {
return listeners.push(listener);
},
}
复制代码
扯远了,有兴趣的可以看 react-router
的源码;
table列搜索和筛选
同理,table列搜索和筛选
和 分页
也需要有状态的(避免刷新和路由跳转返回搜索条件丢失的不便),也记录在 url
上,用 antd
可以通过 onChange
事件拿到。
行右键操作
右键的操作一般都是弹窗或者页面跳转,modal
和 table
的关系只有当前 table
选中要操作行的数据。 如何激活当前的列表项?然后将数据传给列表项对应的 modal 是关键。
antd table
暴露的 onRow
事件正好支持鼠标右键,所以只需右键就能激活操作列表,且能拿到操作行的数据,点击具体操作将对应的数据传入 modal
, 然后通过 setModal
到全局即可激活 modal 。
// tableProps
onRow(record, index) {
...
return {
onContextMenu(e) {
setMenuState({
// 展示右键后当前的菜单
visible: true,
// 设置当前的数据
currentRow: record
});
}
}
}
// 菜单单击事件
handleMenuClick = () => {
...
// 调用上面分析的的useActionCallback显示modal
onAction(key, currentRow);
...
}
复制代码
这样,页面和 modal
组件解耦,只跟根据用户的操作产生的数据来决定。用户点击操作项产生数据,将数据传入 modal
,然后全局激活modal
。
总结
react hook
和 context
结合会发生一些不可思议的事情。
-
context
的发明就是为了父子孙...
组件间共享数据、全局记录数据。 -
Provide
负责传递共享的数据,useContext
负责消费数据,这里的消费包括使用、更新和删除等操作。 -
context
和react hook
可以让页面和一些重复的操作做一些解耦合操作。