跳到主要内容

关于高阶组件的实践

背景

在我刚接触react的时候,就有看到面经频繁提到的高阶组件(High Order Component)理念,说它是一个很重要的理念,可是却只提供了比较简单的场景,例如实现一个权限 校验的高阶组件。

后面在本人自己的工作实践过程中,打磨出一种自认为比较好用的实践,是关于一个对抽屉/弹窗的hoc封装。

为什么需要这种封装?

抽屉/弹窗类组件有一些共同的逻辑:

  • 要被打开/关闭
  • 通常需要在父组件中声明多个状态(XXXVisible, 传入的数据等)
  • 如果有多个串行的抽屉/弹窗表单,代码可能变得难以阅读和维护

我认为用更少的state,更多的compute来实现功能,才是遵循React官方推崇的声明式地考虑 UI

实现

所以我写了关于弹窗/抽屉的高阶组件。

使用了arco-design-react组件

live demo

实时编辑器
function MyPlayground() {
  function DrawerHOC(functionComponent) {
    return forwardRef((props, ref) => {
      const [open, setOpen] = useState(false);
      const [data, setData] = useState({});

      const close = () => {
        setOpen(false);
        props?.onClose?.();
      };

      useImperativeHandle(ref, () => {
        return {
          open(data = null) {
            setOpen(true);
            if (data) setData(data);
          },
          close,
        };
      });

      let children;
      let drawerProps;
      const result = functionComponent({ close, data, open, outerProps: props });
      if (Array.isArray(result)) {
        children = result[0];
        drawerProps = result[1];
      } else {
        children = result;
        drawerProps = {};
      }
      return (
        <Modal
          onCancel={close}
          visible={open}
          unmountOnExit={true}
          {...props}
          {...drawerProps}
        >
          {children}
          {props.children}
        </Modal>
      );
    });
  }

  const DrawerPersonalInfoEdit = DrawerHOC(function ({close, open, data, outerProps}) {
    const [form] = Form.useForm()
    const {userId, onOk = ()=> {}} = data

    useEffect(()=>{ 
      if (!open) return
      setTimeout(() => {
        // 使用userId获取用户信息,再设置表单?
        form.setFieldsValue({
          name: "ybr",
          age: 29
        })
      }, 500);
    }, [open])

    return [
      <div>
        <Form form={form} layout="vertical">
          <Form.Item label="姓名" field="name">
            <Input />
          </Form.Item>
          <Form.Item label="年龄" field="age">
            <InputNumber />
          </Form.Item>
        </Form>
      </div>,
      {
        title: "个人信息",
        width: 500,
        onOk: () => {
          form.validate().then(values => {
            onOk?.(close, values)
          })
        },
        onCancel: () => {
            close()
            form.resetFields()
        }
      }
    ]
  })

  const DrawerDeptInfoEdit = DrawerHOC(function ({close, open, data, outerProps}) {
    const [form] = Form.useForm()
    const {values, onOk = ()=> {}} = data
    return [
      <div>
        <h6>个人信息</h6>
        {JSON.stringify(values)}
        <Form form={form} layout="vertical">
          <Form.Item label="部门" field="deptName">
            <Input />
          </Form.Item>
        </Form>
      </div>,
      {
        title: "部门信息",
        width: 500,
        onOk: () => {
          form.validate().then(values => {
            onOk?.(close, values)
          })
        },
        onCancel: () => {
            close()
            form.resetFields()
        }
      }
    ]
  })

  const drawerPersonalInfoEditRef = useRef()
  const drawerDeptInfoEditRef = useRef()

  return <div>
    <DrawerPersonalInfoEdit ref={drawerPersonalInfoEditRef} />
    <DrawerDeptInfoEdit ref={drawerDeptInfoEditRef} />
    <Button type="primary" onClick={()=>{   
      drawerPersonalInfoEditRef.current.open({
        userId: "1",
        onOk: (drawerPersonalInfoEditClose, values) => {
          drawerPersonalInfoEditClose();
          drawerDeptInfoEditRef.current.open({
            values,
            onOk(drawerDeptInfoEditClose, values2) {
              if (window.confirm("是否关闭")) {
                alert(JSON.stringify({...values, ...values2}))
                drawerDeptInfoEditClose();
              }
            }
          })
        }
      })
    }}>
      编辑
    </Button>
  </div>
}
结果
Loading...

这么使用,有以下好处:

  1. 封装了抽屉/弹窗的通用逻辑,如 open/close 方法,使业务组件可以更专注于实现具体功能。
  2. 业务组件与抽屉/弹窗状态紧密结合,既可以修改状态,也能响应状态变化。例如DrawerPersonalInfoEdit组件只在抽屉打开时获取用户信息。
  3. 简化了串行交互的实现。可以通过回调函数优雅地处理多个抽屉/弹窗之间的数据传递和流程控制。
  4. 减少了在父组件中声明的状态数量,符合 React 官方推荐的"声明式 UI"思想。
  5. 提高了代码的可读性和可维护性,特别是在处理复杂的表单流程时。
  6. 通过 HOC 模式实现了逻辑复用,使得创建新的抽屉/弹窗组件变得更加简单和统一。

总结

这种实践充分利用了 HOC 的优势,在保持灵活性的同时,大大简化了抽屉/弹窗相关的状态管理和交互逻辑。