由来: 需要封装一个通过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组件内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

React封装CustomSelect组件思路详解的更多相关文章

  1. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  2. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  3. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  4. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  5. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  6. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  7. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

  8. 反应原生 – 如何通过Xcode构建React Native iOS应用程序到设备?

    我试图将AwesomeProject应用程序构建到设备上.构建成功并启动屏幕显示,但后来我看到一个红色的“无法连接到开发服务器”屏幕.它表示“确保节点服务器正在运行–从Reactroot运行”npmstart“.看起来节点服务器已经运行,因为当我做npm启动时,我收到一个EADDRINUSE消息,表示该端口已经在使用.解决方法从设备访问开发服务器您可以使用开发服务器快速迭代设备.要做到这一点,你的

  9. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

  10. 如何为iOS的React Native设置分析

    所以我已经完成了一个针对iOS的ReactNative项目,但是我想在其中分析.我尝试了react-native-google-analytics软件包,但是问题阻止了它的正常工作.此外,react-native-cordova-plugin软件包只适用于Android,因此插入Cordova插件进行分析的能力现在已成为问题.我也没有Swift/ObjectiveC的经验,所以将完全失去GA的插入.有没有人有任何建议如何连接GoogleAnalytics的ReactNativeforiOS?

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部