React封装CustomSelect组件思路详解

目录
  • 思路和前提
  • 编码与实现
  • 处理createContext与useContext
  • 对content的封装和拆分: DispatchRender, Controls
  • 先说Controls, 包含控制行: 重置, 确定
  • DispatchRender 用于根据type分发对应的render子组件,这是一种编程思想,在次可以保证父子很大程度的解耦,再往下子组件不再考虑type是什么,父组件不需要考虑子组件有什么。
  • 单选框的render子组件的具体实现

由来: 需要封装一个通过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) => (
          <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有很大的考虑余地,是整个组件设计的根本要素之一,传什么参数决定了你后续的设计,以及这个组件是否显得"很傻",是否简单好用,以及后续如果想添加功能是否只能重构
  • 另一个核心要素是数据流: 组件内部的数据流如何清晰而方便的控制,又如何与外层调用组件交互,也直接决定了组件的复杂度。
  • 一些组件封装的经验和模式:比如复杂的核心方法可以考虑使用柯里化根据参数重要性分层传入;复杂的多类别的子组件可以用分发模式解耦;以及一些像单一职责,高内聚低耦合等灵活应用这些理论知识。

到此这篇关于React封装CustomSelect组件思路的文章就介绍到这了,更多相关React封装CustomSelect组件内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 从零搭建react+ts组件库(封装antd)的详细过程

    目录 整体需求 开发与打包工具选型 使用webpack作为打包工具 使用babel来处理typescript代码 使用less-loader.css-loader等处理样式代码 项目搭建思路 整体结构 方案思路 项目搭建实施 初始化 Babel引入 了解Babel webpack的基于babel-loader的处理流程 引入React相关库(externals方式) 编写组件代码 编译打包组件库 效果演示 处理样式(less编译与css导出) 依赖引入 配置webpack 编写样式代码 编译组件

  • React-View-UI组件库封装Loading加载中源码

    目录 组件介绍 Loading API能力 组件源码 组件测试源码 组件库线上地址 组件介绍 Loading组件是日常开发用的很多的组件,这次封装主要包含两种状态的Loading,旋转.省略号,话不多说先看一下组件的文档页面吧: Loading API能力 组件一共提供了如下的API能力,可以在使用时更灵活: type表示loading类型,默认是default,当用户需要使用省略样式,设置type=dot即可: mask配置蒙层,可在loading时遮挡覆盖内容为半透明状态,适用于内容未加载时

  • 基于React封装组件的实现步骤

    目录 前言 antd 是如何封装组件的 divider 组件源代码 如何暴露组件属性 如何设置统一类名前缀 如何处理样式与类名 divider 组件样式源代码 前言 很多小伙伴在第一次尝试封装组件时会和我一样碰到许多问题,比如人家的组件会有 color 属性,我们在使用组件时传入组件文档中说明的属性值如 primary ,那么这个组件的字体颜色会变为 primary 对应的颜色,这是如何做到的?还有别人封装的组件类名都有自己独特的前缀,这是如何处理的呢,难道是 css 类名全部加上前缀吗,这也太

  • React Form组件的实现封装杂谈

    前言 对于网页系统来说,表单提交是一种很常见的与用户交互的方式,比如提交订单的时候,需要输入收件人.手机号.地址等信息,又或者对系统进行设置的时候,需要填写一些个人偏好的信息. 表单提交是一种结构化的操作,可以通过封装一些通用的功能达到简化开发的目的.本文将讨论Form表单组件设计的思路,并结合有赞的ZentForm组件介绍具体的实现方式.本文所涉及的代码都是基于React v15的版本. Form组件功能 一般来说,Form组件的功能包括以下几点: 表单布局 表单字段 封装表单验证&错误提示

  • React封装CustomSelect组件思路详解

    目录 思路和前提 编码与实现 处理createContext与useContext 对content的封装和拆分: DispatchRender, Controls 先说Controls, 包含控制行: 重置, 确定 DispatchRender 用于根据type分发对应的render子组件,这是一种编程思想,在次可以保证父子很大程度的解耦,再往下子组件不再考虑type是什么,父组件不需要考虑子组件有什么. 单选框的render子组件的具体实现 由来: 需要封装一个通过Popover弹出框里可以

  • react component function组件使用详解

    目录 不可改变性 虚拟dom与真实dom 函数组件 组件复用 纯函数 组件组合--组件树 组件抽离 不可改变性 1.jsx- 2.component(function)-component(class)-components(函数组件组合)-component tree(redux)-app(项目开发) 在react中,创建了js对象(react元素)就是不可更改的(immutable).就像是用相机拍照,相当于在此时间点已经定位了时间节点,只能拍下一张照片. 例如,使用底层react写一个时钟

  • js中自定义react数据验证组件实例详解

    我们在做前端表单提交时,经常会遇到要对表单中的数据进行校验的问题.如果用户提交的数据不合法,例如格式不正确.非数字类型.超过最大长度.是否必填项.最大值和最小值等等,我们需要在相应的地方给出提示信息.如果用户修正了数据,我们还要将提示信息隐藏起来. 有一些现成的插件可以让你非常方便地实现这一功能,如果你使用的是knockout框架,那么你可以借助于Knockout-Validation这一插件.使用起来很简单,例如我下面的这一段代码: ko.validation.locale('zh-CN');

  • React中父子组件通信详解

    目录 父组件向子组件通信 存在期 父组件向子组件通信 在父组件中,为子组件添加属性数据,即可实现父组件向子组件通信.传递的数据可以分成两类 子组件是作为属性来接收这些数据的 第一类就是数据:变量,对象,属性数据,状态数据等等 这些数据发生改变,子组件接收的属性数据就发生了改变. 第二类就是方法:父组件可以向子组件传递属性方法,子组件接收方法,并可以在组件内执行,有两种执行方式 注意:父组件传给子组件的方法是不能执行的,执行了相当于将方法的返回值传递给子组件. 第一种 作为事件回调函数执行 参数默

  • vue封装tabs组件案例详解

    本文实例为大家分享了vue封装tabs组件案例的具体代码,供大家参考,具体内容如下 回顾封装tabs组件熟知运用$children,$parent的案例生成整个容器,通过$children拿取子组件 <template>   <div class="ll-tabs">     <div class="ll-tabs__header">       <div         class="ll-tabs__heade

  • 基于jQuery.validate及Bootstrap的tooltip开发气泡样式的表单校验组件思路详解

    表单校验是页面开发中非常常见的一类需求,相信每个前端开发人员都有这方面的经验.网上有很多成熟的表单校验框架,虽然按照它们默认的设计,用起来没有多大的问题,但是在实际工作中,表单校验有可能有比较复杂的个性化的需求,使得我们用这些插件的默认机制并不能完成这些功能,所以要根据自己的需要去改造它们(毕竟自己还不到那个写一个完美的校验框架的层次).我用过formValidation这个校验框架,虽然它跟bootstrap配合地很好,但是校验风格太死板,不太满足个性化的场景:后来我找到了jquery.val

  • 使用Vue制作图片轮播组件思路详解

    之前一直都没有认真的写过一个组件.以前在写业务代码的过程中,都是用的别人封装好的组件,这次尝试着写了一个图片轮播组件,虽然比不上知名的轮播组件,但它的功能基本完整,而且在写这个组件的过程中,学的东西也很多,在这里也给大家分享出来,如有疏漏,欢迎指正! 在制作这个组件之前,笔者google了不少关于轮播的文章,发现实现一个轮播的思路虽然各有不同,但是大的逻辑其实差不多,本文主要依据慕课网上焦点轮播图特效这节课,不过慕课网主要用原生JS写,而笔者则用Vue进行了重构,并且进行了一点修改.完成后的组件

  • vue远程加载sfc组件思路详解

    问题 在我们的 vue 项目中(特别是后台系统),总会出现一些需要多业务线共同开发同一个项目的场景,如果各业务团队向项目中提供一些公共业务组件,但是这些组件并不能和项目一起打包,因为项目中不能因为某个私有模块的频繁变更而重复构建发布. ^_^不建议在生产环境使用,代码包含eval 思路 在这种场景下我们需要将公共的业务组件部署到服务端,由客户端请求并渲染组件. 服务端解析.vue文件 使用vue-template-compiler 模板解析器,解析SFC(单文件组件) const compile

  • 对jquery的ajax进行二次封装以及ajax缓存代理组件:AjaxCache详解

    虽然jquery的较新的api已经很好用了, 但是在实际工作还是有做二次封装的必要,好处有:1,二次封装后的API更加简洁,更符合个人的使用习惯:2,可以对ajax操作做一些统一处理,比如追加随机数或其它参数.同时在工作中,我们还会发现,有一些ajax请求的数据,对实时性要求不高,即使我们把第一次请求到的这些数据缓存起来,然后当相同请求再次发起时直接拿之前缓存的数据返回也不会对相关功能有影响,通过这种手工的缓存控制,减少了ajax请求,多多少少也能帮助我们提高网页的性能.本文介绍我自己关于这两方

  • vue单页开发父子组件传值思路详解

    vue单页开发时经常需要父子组件之间传值,自己用过但是不是很熟练,这里我抽空整理了一下思路,写写自己的总结. GitHub地址:https://github.com/leileibrother/wechat-vue 文件目录如下图,example.vue是父组件,exampleChild.vue是子组件. 父组件引入子组件,父组件html的代码如下: <template> <div> <h3>这是父组件</h3> <p style="marg

随机推荐