随心而记
1054 字
5 分钟
使用 ReactDOM.createPortal 重写组件
2024-07-17
发布时间
分类
标签

最近在做一个新项目,这个项目的有一个交互的规范是搜索条件默认是隐藏的然后点击一个过滤器 Icon,展示那些搜索条件,由于我们部门是后来加入的这个项目,在此基础上做一些定制开发,也要遵循这一规范,但是我看到已经有其他开发编写完成了这个组件,但是我认为这样编写的组件扩展性和可维护性比较差,因此我打算使用 ReactDOM.createPortal 重写这个组件。

组件预览#

原组件#

目前这个组件的使用方式,可以看到,实现这个组件需要引入两个组件,分别是 FilterIconFilterView ,其中 FilterIcon 是那个过滤器的 Icon 主要作用是点击展示隐藏搜索条件。

FilterView 主要是定义一些搜索条件,它这里将那些条件 抽象了出来并传入 searchList

// 示例伪代码
import FilterIcon from "@/pages/LuZhiBI/components/FilterIcon/filterIcon";
import FilterView from "@/pages/LuZhiBI/components/FilterView";

const searchList: any[] = [
  {
    searchType: "string",
    searchComponentType: "input",
    placeHolder: "请输入指标名称",
    title: "指标名称",
    key: "indicatorName",
  },
  {
    searchType: "string",
    searchComponentType: "multipleSelect",
    placeHolder: "请选择指标类型",
    title: "指标类型",
    key: "indicatorType",
    options: options.classificationOfIndicatorsOptions,
    mode: "multiple",
    defaultValue: options.classificationOfIndicatorsOptions.map(item => item.value),
  },
  {
    searchType: "text",
    searchComponentType: "dateRangeTime",
    showTime: true,
    title: "创建时间",
    key: "createdAt",
  },
];

<div className={styles.container}>
  <div className={styles.headBox}>
    ...
    <div className={styles.searchBox}>
      ...
      <FilterIcon handleFilterClick={handleFilterClick} filterTitle="过滤器" />
      ...
    </div>
  </div>

  <FilterView ref={filterViewRef} searchList={searchList} finish={onFinish} defaultFormValue={defaultSearchInfo} />
</div>

从上可以看出,有几点不足之处

  • 一个功能却需要引入两个组件配合
  • 需要手动获取 FilterView ref,并使用 handleFilterClick 控制展示隐藏
  • searchList 定义复杂,并且不利于自定义的一些配置(需要修改原组件,在内部做一些业务适配,随着项目的不断迭代,组件内部会变得相当庞大,且难以维护,之前做的一个项目就出现了类似的问题),在定义searchList 时没有 typescript 提示,失去了使用它的意义。

新组件#

在打算写这个组件之前,我想到了 Vue 中的 **Teleport** 内置组件,它可以将组件内部的元素传送到目标 dom 节点位置,因此我在想是否 React 中有一个类似的装件,然后我就发现了**ReactDOM.createPortal**,它可以实现类似 Teleport 的功能。

详细解释#

ReactDOM.createPortal:该方法接受两个参数:第一个参数是渲染的子元素,第二个参数是目标 DOM 节点。

对于现在的需求,这个组件需要实现两部分,一个是固定的过滤器 Icon,另一个是需要被传送到目前位置的 children,最终实现代码如下。

import { type FC, useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { Button, Tooltip } from 'antd';
import { FilterOutlined } from '@ant-design/icons';
import { motion } from 'framer-motion';

interface IProps {
  hoverTitle: string;
  nodeID: string;
  children?: React.ReactNode;
}

const FilterIcon: FC<IProps> = props => {
  const { hoverTitle = '过滤器', nodeID, children } = props;
  const [node, setNode] = useState<any>(null);
  const [height, setHeight] = useState<string | number>(0);
  const filterClick = () => {
    if (height === 0) {
      setHeight('auto');
    } else {
      setHeight(0);
    }
  };
  useEffect(() => {
    const targetNode = document.getElementById(nodeID);
    setNode(targetNode);
  }, [nodeID]);
  return (
    <>
      <Tooltip placement="bottom" title={hoverTitle}>
        <Button
          onClick={filterClick}
          icon={
            <FilterOutlined
              style={{
                fontSize: 16,
              }}
            />
          }
          type="text"
        ></Button>
      </Tooltip>
      {node &&
        ReactDOM.createPortal(
          <motion.div
            animate={{ height }}
            className="filter-conditions"
            style={{
              height: 0,
              overflow: 'hidden',
              width: '100%',
              backgroundColor: '#f5f5f5',
              borderRadius: 4,
              marginTop: 10,
              padding: '0 16px',
            }}
          >
            {children}
          </motion.div>,
          node,
        )}
    </>
  );
};
export default FilterIcon;

使用方式#

使用时只需要引入 Filter ,并在合适的位置建立一个空的 div 节点 <div id="filter-main" />,并指定节点 id,并将它传入给 Filter 组件即可,过滤条件则直接作为 children 写入 Filter 组件内部即可。

// 示例伪代码
import Filter from '@/pages/LuZhiBI/components/Filter';

<div className={styles.DatasetView}>
  <div className={styles.titleBox}>
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
      <div className={styles.filterBox}>
        <Filter hoverTitle="过滤器" nodeID="filter-main">
          <Form 
          name="filter"
          onFinish={onFinish}
          autoComplete="off"
          initialValues={{
            date: [],
            indicator: [''],
          }}
          >
            <Row gutter={16} style={{ marginTop: 16 }}>
              <Col span={8}>
                <Form.Item<FieldType>
                  label="日期"
                  name="date"
                  rules={[{ required: true, message: '请选择查询日期' }]}
                >
                  <RangePicker style={{ width: '100%' }} />
                </Form.Item>
              </Col>
              <Col span={8}>
                <Form.Item<FieldType>
                  label="指标"
                  name="indicator"
                  required
                  rules={[{
                    validator: (_rule, value) => {
                      if (!value && value !== '') {
                        return Promise.reject('请选择指标');
                      }
                      return Promise.resolve();
                    }
                  }]}
                >
                  <Select mode='multiple' options={indicatorOptions} />
                </Form.Item>
              </Col>
              <Col span={8}>
                <Flex justify="flex-end">
                  <Form.Item>
                    <Space size="small">
                      <Button htmlType="reset">
                        重置
                      </Button>
                      <Button type="primary" htmlType="submit">
                        查询
                      </Button>
                    </Space>
                  </Form.Item>
                </Flex>
              </Col>
            </Row>
          </Form>
        </Filter>
      </div>
    </div>
    <div id="filter-main" />
  </div>
</div>
使用 ReactDOM.createPortal 重写组件
https://www.promises.top/posts/rewriting-components-with-reactdomcreateportal/
作者
发布于
2024-07-17
许可协议
CC BY-NC-SA 4.0