资讯专栏INFORMATION COLUMN

React封装CustomSelect组件思路详解

3403771864 / 229人阅读

  前提:一个通过Popover弹出框里自定义渲染内容的组件要进行封装,目前要求实现有: 单选框, 复选框。我们需要考虑封装组件时要权衡组件的灵活性, 拓展性以及代码的优雅规范,现在和大家一起分享。

  思路和前提

  在层级较多,组件较为多的情况下,为了方便使用了React.createContext + useContext作为参数向下传递的方式。

 我们要先确定要antd的Popover组件是继承自Tooltip组件的,而CustomSelect组件是继承自Popover组件的。对于要对某个组件进行二次封装,其props类型一般有两种方式处理: 继承, 合并。

  interface IProps extends XXX;
  type IProps = Omit<TooltipProps, 'overlay'> & {...};

  Popover的触发类型中有一个重要: trigger,在默认中有四种"hover" "focus" "click" "contextMenu", 并且可以使用数组设置多个触发行为。今天我们只需要"hover"和"click", 对该字段进行覆盖。

  对于Select, Checkbox这种表单控件来说,对齐二次封装,不少时候都要对采用'受控组件'的方案,通过'value' + 'onChange'的方式"接管"其数据的输入和输出。注意value不是必传的,在使用组件时只获取操作的数据,传入value更多是做的一个初始值。onChange也是唯一的出口数值,这是很有必要,不然你怎么获取的到操作的数据呢?是吧。

  说一个要点: 既然表单控件时单选框,复选框, 那我们的输入一边是string, 一边是string[],既大大增加了编码的复杂度,也增加了使用的心智成本。所以我这里的想法是统一使用string[], 而再单选的交互就是用value[0]等方式完成单选值与数组的转换。

  编码与实现

  // types.ts
  import type { TooltipProps } from 'antd';
  interface OptItem {
  id: string;
  name: string;
  disabled: boolean; // 是否不可选
  children?: OptItem[]; // 递归嵌套
  }
  // 组件调用的props传参
  export type IProps = Omit<TooltipProps, 'overlay' | 'trigger'> & {
  /** 选项类型: 单选, 复选 */
  type: 'radio' | 'checkbox';
  /** 选项列表 */
  options: OptItem[];
  /** 展示文本 */
  placeholder?: string;
  /** 触发行为 */
  trigger?: 'click' | 'hover';
  /** 受控组件: value + onChange 组合 */
  value?: string[];
  onChange?: (v: string[]) => void;
  /** 样式间隔 */
  size?: number;
  }

  处理createContext与useContext

  import type { Dispatch, MutableRefObj, SetStateAction } from 'react';
  import { createContext } from 'react';
  import type { IProps } from './types';
  export const Ctx = createContext<{
  options: IProps['options'];
  size?: number;
  type: IProps['type'];
  onChange?: IProps['onChange'];
  value?: IProps['value'];
  // 这里有两个额外的状态: shadowValue表示内部的数据状态
  shadowValue: string[];
  setShadowValue?: Dispatch<SetStateAction<string[]>>;
  // 操作弹出框
  setVisible?: (value: boolean) => void;
  // 复选框的引用, 暴露内部的reset方法
  checkboxRef?: MutableRefObject<{
  reset: () => void;
  } | null>;
  }>({ options: [], shadowValue: [], type: 'radio' });


  // index.tsx
  /**
  * 自定义下拉选择框, 包括单选, 多选。
  */
  import { FilterOutlined } from '@ant-design/icons';
  import { useBoolean } from 'ahooks';
  import { Popover } from 'antd';
  import classnames from 'classnames';
  import { cloneDeep } from 'lodash';
  import type { FC, ReactElement } from 'react';
  import { memo, useEffect, useRef, useState } from 'react';
  import { Ctx } from './config';
  import Controls from './Controls';
  import DispatchRender from './DispatchRender';
  import Styles from './index.less';
  import type { IProps } from './types';
  const Index: FC<IProps> = ({
  type,
  options,
  placeholder = '筛选文本',
  trigger = 'click',
  value,
  onChange,
  size = 6,
  style,
  className,
  ...rest
  }): ReactElement => {
  // 弹窗显示控制(受控组件)
  const [visible, { set: setVisible }] = useBoolean(false);
  // checkbox专用, 用于获取暴露的reset方法
  const checkboxRef = useRef<{ reset: () => void } | null>(null);
  // 内部维护的value, 不对外暴露. 统一为数组形式
  const [shadowValue, setShadowValue] = useState<string[]>([]);
  // value同步到中间状态
  useEffect(() => {
  if (value && value?.length) {
  setShadowValue(cloneDeep(value));
  } else {
  setShadowValue([]);
  }
  }, [value]);
  return (
  <Ctx.Provider
  value={{
  options,
  shadowValue,
  setShadowValue,
  onChange,
  setVisible,
  value,
  size,
  type,
  checkboxRef,
  }}
  >
  <Popover
  visible={visible}
  onVisibleChange={(vis) => {
  setVisible(vis);
  // 这里是理解难点: 如果通过点击空白处关闭了弹出框, 而不是点击确定关闭, 需要额外触发onChange, 更新数据。
  if (vis === false && onChange) {
  onChange(shadowValue);
  }
  }}
  placement="bottom"
  trigger={trigger}
  content={
  <div className={Styles.content}>
  {/* 分发自定义的子组件内容 */}
  <DispatchRender type={type} />
  {/* 控制行 */}
  <Controls />
  </div>
  }
  {...rest}
  >
  <span className={classnames(Styles.popoverClass, className)} style={style}>
  {placeholder ?? '筛选文本'}
  <FilterOutlined style={{ marginTop: 4, marginLeft: 3 }} />
  </span>
  </Popover>
  </Ctx.Provider>
  );
  };
  const CustomSelect = memo(Index);
  export { CustomSelect };
  export type { IProps };
  对content的封装和拆分: DispatchRender, Controls

  先说Controls, 包含控制行: 重置, 确定

  /** 控制按钮行: "重置", "确定" */
  import { Button } from 'antd';
  import { cloneDeep } from 'lodash';
  import type { FC } from 'react';
  import { useContext } from 'react';
  import { Ctx } from './config';
  import Styles from './index.less';
  const Index: FC = () => {
  const { onChange, shadowValue, setShadowValue, checkboxRef, setVisible, value, type } =
  useContext(Ctx);
  return (
  <div className={Styles.btnsLine}>
  <Button
  type="primary"
  ghost
  size="small"
  onClick={() => {
  // radio: 直接重置为value
  if (type === 'radio') {
  if (value && value?.length) {
  setShadowValue?.(cloneDeep(value));
  } else {
  setShadowValue?.([]);
  }
  }
  // checkbox: 因为还需要处理全选, 需要交给内部处理
  if (type === 'checkbox') {
  checkboxRef?.current?.reset();
  }
  }}
  >
  重置
  </Button>
  <Button
  type="primary"
  size="small"
  onClick={() => {
  if (onChange) {
  onChange(shadowValue); // 点击确定才触发onChange事件, 暴露内部数据给外层组件
  }
  setVisible?.(false); // 关闭弹窗
  }}
  >
  确定
  </Button>
  </div>
  );
  };
  export default Index;

  DispatchRender 用于根据type分发对应的render子组件,这是一种编程思想,在次可以保证父子很大程度的解耦,再往下子组件不再考虑type是什么,父组件不需要考虑子组件有什么。

 

  /** 分发详情的组件,保留其可拓展性 */
  import type { FC, ReactElement } from 'react';
  import CheckboxRender from './CheckboxRender';
  import RadioRender from './RadioRender';
  import type { IProps } from './types';
  const Index: FC<{ type: IProps['type'] }> = ({ type }): ReactElement => {
  let res: ReactElement = <></>;
  switch (type) {
  case 'radio':
  res = <RadioRender />;
  break;
  case 'checkbox':
  res = <CheckboxRender />;
  break;
  default:
  // never作用于分支的完整性检查
  ((t) => {
  throw new Error(`Unexpected type: ${t}!`);
  })(type);
  }
  return res;
  };
  export default Index;

  单选框的render子组件的具体实现


  import { Radio, Space } from 'antd';
  import type { FC, ReactElement } from 'react';
  import { memo, useContext } from 'react';
  import { Ctx } from './config';
  const Index: FC = (): ReactElement => {
  const { size, options, shadowValue, setShadowValue } = useContext(Ctx);
  return (
  <Radio.Group
  value={shadowValue?.[0]} // Radio 接受单个数据
  onChange={({ target }) => {
  // 更新数据
  if (target.value) {
  setShadowValue?.([target.value]);
  } else {
  setShadowValue?.([]);
  }
  }}
  >
  <Space direction="vertical" size={size ?? 6}>
  {options?.map((item) => (</p>
  <p>
  <Radio key={item.id} value={item.id}>
  {item.name}
  </Radio>
  ))}
  </Space>
  </Radio.Group>
  );
  };
  export default memo(Index);

  个人总结

  typescript作为组件设计和一点点推进的好助,可以实现:继承,合并,, 类型别名,类型映射(Omit, Pick, Record), never分支完整性检查等.通常每个组件多带带有个types.ts文件统一管理所有的类型,组件入口props有很大的考虑余地,这是整个组件设计的根本要素之一,至于后续传导什么参数,是否好用都是在再考量。

  还有一个要点就是数据流: 组件内部的数据流如何清晰而方便的控制,也要考量如何与外层调用组件交互,这样就直接决定了组件的复杂度。

  经验分享:比如复杂的核心方法在里面可以使用柯里化根据参数重要性分层传入;对于复杂的多类别的子组件看使用分发模式解耦;简单些的考虑用高内聚低耦合等灵活应用这些理论知识。


文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/127736.html

相关文章

  • react进阶系列:高阶组件详解(一)

    摘要:创建一个普通函数因为的存在所以变成构造函数创建一个方法在方法中,创建一个中间实例对中间实例经过逻辑处理之后返回使用方法创建实例而恰好,高阶组件的创建逻辑与使用,与这里的方法完全一致。因为方法其实就是构造函数的高阶组件。 很多人写文章喜欢把问题复杂化,因此当我学习高阶组件的时候,查阅到的很多文章都给人一种高阶组件高深莫测的感觉。但是事实上却未必。 有一个词叫做封装。相信写代码这么久了,大...

    NervosNetwork 评论0 收藏0
  • 详解react、redux、react-redux之间的关系

    摘要:或者兄弟组件之间想要共享某些数据,也不是很方便传递获取等。后面要讲到的就是通过让各个子组件拿到中的数据的。所以,确实和没有什么本质关系,可以结合其他库正常使用。 本文介绍了react、redux、react-redux之间的关系,分享给大家,也给自己留个笔记,具体如下: React 一些小型项目,只使用 React 完全够用了,数据管理使用props、state即可,那什么时候需要引入...

    xioqua 评论0 收藏0
  • react进阶系列 - 高阶组件详解四:高阶组件的嵌套使用

    摘要:前面有讲到过很多页面会在初始时验证登录状态与用户角色。这个时候就涉及到一个高阶组件的嵌套使用。而每一个高阶组件函数执行之后中所返回的组件,刚好可以作为下一个高阶组件的参数继续执行,而并不会影响基础组件中所获得的新能力。 前面有讲到过很多页面会在初始时验证登录状态与用户角色。我们可以使用高阶组件来封装这部分验证逻辑。封装好之后我们在使用的时候就可以如下: export default w...

    LMou 评论0 收藏0

发表评论

0条评论

3403771864

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<