React Hook 系列(三):记一次中台项目的Hook沉淀

image

背景

本文旨在分享,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 的一次生命周期基本包括:
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a0765b86ad4c479b9f622f943523e07c~tplv-k3u1fbpfcp-zoom-1.image" alt="图片替换文本" width="150" height="203" align="bottom" />

特点:

  • modal 的打开和关闭由用户操作决定。
  • 需要记录每次选中的数据,传给要操作的 modal
  • 点击提交成功后都需要关闭 modal 和页面触发刷新的操作。

每次只有一个激活的modal和选中的数据一一对应,两者都是用操作的一瞬间确定的,且每次只有一个 modal 处于激活状态,所以用户的各种操作只是不断更新modaldata而已,所以假如全局有一个的专门记录 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>;

至此,contexthook 让页面和 modal 解耦,它们的联系只有 data, 而 data 又作为参数随时可以传入。
这优雅的写法,是不是让你耳目一新,心动了。

最后再看下modal的内幕modalvisible 参数默认是true,当setModal后挂载,它才会被弹出显示。当 modal 关闭时,需要将全局挂载的 modal 置空,所以把全局ModalContainer记录的modal置空即可。

// useActionModal 自定义hook 主要获取 modalProps
// modal关闭事件中置空
afterClose() {
  // ...
  setModal(null);
},
// ...

2. 高度一致化的 table

在中台项目中,table 列表是很多模块的首页,基本包含 table顶部输入框搜索table列搜索和筛选分页行右键操作自定义列 等。
每个列表页面的不同点:列定义、数据、数据来源,其他的内容基本复制粘贴一把梭。如何解决这中无脑搬砖的活异常重要。
最终我们层层封装,将数据和操作暴露出来,其余搬砖部分通过 useHook 和 Context 全部封装。
下面分析一下我们的解法。

table顶部输入框搜索

table顶部输入框搜索 输入框的内容和列表数据的来源相关。
需求: 在跳转详情后返回,输入框的内容需要保持跳转前一样且执行查询数据,所以记录这个输入框的内容是关键。
解决方法: history statelocalStorage页面公共部分隐藏域URL queryString

显而易见 URL 记录更容易分享、收藏和更直观的展示。最终 table顶部输入框搜索的关键字 被记录在 url querystring中,只需要观察监听 URL的querystring变化即可,这里用到了 react-routeruseLocation 即可监听。

那么问题来了,useLocation 是如何监听 url 的变化的?

阅读源码可知,react-router 也是通过 contextlocation history 统一管理及传递。然后通过监听浏览器 historypopstate 事件来触发更新 contextlocation

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 事件拿到。

行右键操作

右键的操作一般都是弹窗或者页面跳转,modaltable 的关系只有当前 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 hookcontext 结合会发生一些不可思议的事情。

  • context 的发明就是为了父子孙...组件间共享数据、全局记录数据。
  • Provide 负责传递共享的数据,useContext 负责消费数据,这里的消费包括使用、更新和删除等操作。
  • contextreact hook 可以让页面和一些重复的操作做一些解耦合操作。

【React Hook系列】

客官,素质三连吧!???

image


前端学习路-郭盖
此专栏旨在记录开发过程中自己觉得需要记录的、值得分享的知识点和技术坑位,能帮到别人我将非常荣幸, ...
1.1k 声望
312 粉丝
0 条评论
推荐阅读
单文件组件下的vue,可以擦出怎样的火花
与时俱进吧,看着 vue3 和 vite,虽然不会用,但还是心痒痒,然后就把原先基于 vue@2 的实现做了重构。不周之处,大家见谅!下面关于过期的内容,我就用删除线标记了。

leftstick64阅读 45.1k评论 18

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木142阅读 11.9k评论 10

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木60阅读 6k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.2k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木39阅读 7.1k评论 6

CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(&gt;^ω^&lt...

XboxYan42阅读 2.8k评论 14

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan34阅读 2.2k评论 2

封面图
1.1k 声望
312 粉丝
宣传栏