React封裝CustomSelect組件思路詳解
由來(lái): 需要封裝一個(gè)通過(guò)Popover彈出框里可以自定義渲染內(nèi)容的組件,渲染內(nèi)容暫時(shí)有: 單選框, 復(fù)選框。在封裝組件時(shí)我們需要權(quán)衡組件的靈活性, 拓展性以及代碼的優(yōu)雅規(guī)范,總結(jié)分享少許經(jīng)驗(yàn)。

思路和前提
由于考慮組件拆分得比較細(xì),層級(jí)比較多,為了方便使用了
React.createContext + useContext作為參數(shù)向下傳遞的方式。
首先需要知道antd的Popover組件是繼承自Tooltip組件的,而我們的CustomSelect組件是繼承自Popover組件的。對(duì)于這種基于某個(gè)組件的二次封裝,其props類型一般有兩種方式處理: 繼承, 合并。
interface IProps extends XXX;
type IProps = Omit<TooltipProps, 'overlay'> & {...};對(duì)于Popover有個(gè)很重要的觸發(fā)類型: trigger,默認(rèn)有四種"hover" "focus" "click" "contextMenu", 并且可以使用數(shù)組設(shè)置多個(gè)觸發(fā)行為。但是我們的需求只需要"hover"和"click", 所以需要對(duì)該字段進(jìn)行覆蓋。
對(duì)于Select, Checkbox這種表單控件來(lái)說(shuō),對(duì)齊二次封裝,很多時(shí)候需要進(jìn)行采用'受控組件'的方案,通過(guò)'value' + 'onChange'的方式"接管"其數(shù)據(jù)的輸入和輸出。并且value不是必傳的,使用組件時(shí)可以單純的只獲取操作的數(shù)據(jù),傳入value更多是做的一個(gè)初始值。而onChange是數(shù)據(jù)的唯一出口,我覺得應(yīng)該是必傳的,不然你怎么獲取的到操作的數(shù)據(jù)呢?對(duì)吧。
有一個(gè)注意點(diǎn): 既然表單控件時(shí)單選框,復(fù)選框, 那我們的輸入一邊是string, 一邊是string[],既大大增加了編碼的復(fù)雜度,也增加了使用的心智成本。所以我這里的想法是統(tǒng)一使用string[], 而再單選的交互就是用value[0]等方式完成單選值與數(shù)組的轉(zhuǎn)換。
編碼與實(shí)現(xiàn)
// types.ts
import type { TooltipProps } from 'antd';
interface OptItem {
id: string;
name: string;
disabled: boolean; // 是否不可選
children?: OptItem[]; // 遞歸嵌套
}
// 組件調(diào)用的props傳參
export type IProps = Omit<TooltipProps, 'overlay' | 'trigger'> & {
/** 選項(xiàng)類型: 單選, 復(fù)選 */
type: 'radio' | 'checkbox';
/** 選項(xiàng)列表 */
options: OptItem[];
/** 展示文本 */
placeholder?: string;
/** 觸發(fā)行為 */
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'];
// 這里有兩個(gè)額外的狀態(tài): shadowValue表示內(nèi)部的數(shù)據(jù)狀態(tài)
shadowValue: string[];
setShadowValue?: Dispatch<SetStateAction<string[]>>;
// 操作彈出框
setVisible?: (value: boolean) => void;
// 復(fù)選框的引用, 暴露內(nèi)部的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);
// 內(nèi)部維護(hù)的value, 不對(duì)外暴露. 統(tǒng)一為數(shù)組形式
const [shadowValue, setShadowValue] = useState<string[]>([]);
// value同步到中間狀態(tài)
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);
// 這里是理解難點(diǎn): 如果通過(guò)點(diǎn)擊空白處關(guān)閉了彈出框, 而不是點(diǎn)擊確定關(guān)閉, 需要額外觸發(fā)onChange, 更新數(shù)據(jù)。
if (vis === false && onChange) {
onChange(shadowValue);
}
}}
placement="bottom"
trigger={trigger}
content={
<div className={Styles.content}>
{/* 分發(fā)自定義的子組件內(nèi)容 */}
<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 };對(duì)content的封裝和拆分: DispatchRender, Controls
先說(shuō)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: 因?yàn)檫€需要處理全選, 需要交給內(nèi)部處理
if (type === 'checkbox') {
checkboxRef?.current?.reset();
}
}}
>
重置
</Button>
<Button
type="primary"
size="small"
onClick={() => {
if (onChange) {
onChange(shadowValue); // 點(diǎn)擊確定才觸發(fā)onChange事件, 暴露內(nèi)部數(shù)據(jù)給外層組件
}
setVisible?.(false); // 關(guān)閉彈窗
}}
>
確定
</Button>
</div>
);
};
export default Index;DispatchRender 用于根據(jù)type分發(fā)對(duì)應(yīng)的render子組件,這是一種編程思想,在次可以保證父子很大程度的解耦,再往下子組件不再考慮type是什么,父組件不需要考慮子組件有什么。
/** 分發(fā)詳情的組件,保留其可拓展性 */
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子組件的具體實(shí)現(xiàn)
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 接受單個(gè)數(shù)據(jù)
onChange={({ target }) => {
// 更新數(shù)據(jù)
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);個(gè)人總結(jié)
- 用好typescript作為你組件設(shè)計(jì)和一點(diǎn)點(diǎn)推進(jìn)的好助手,用好:繼承,合并,, 類型別名,類型映射(Omit, Pick, Record), never分支完整性檢查等. 一般每個(gè)組件單獨(dú)有個(gè)types.ts文件統(tǒng)一管理所有的類型
- 組件入口props有很大的考慮余地,是整個(gè)組件設(shè)計(jì)的根本要素之一,傳什么參數(shù)決定了你后續(xù)的設(shè)計(jì),以及這個(gè)組件是否顯得"很傻",是否簡(jiǎn)單好用,以及后續(xù)如果想添加功能是否只能重構(gòu)
- 另一個(gè)核心要素是數(shù)據(jù)流: 組件內(nèi)部的數(shù)據(jù)流如何清晰而方便的控制,又如何與外層調(diào)用組件交互,也直接決定了組件的復(fù)雜度。
- 一些組件封裝的經(jīng)驗(yàn)和模式:比如復(fù)雜的核心方法可以考慮使用柯里化根據(jù)參數(shù)重要性分層傳入;復(fù)雜的多類別的子組件可以用分發(fā)模式解耦;以及一些像單一職責(zé),高內(nèi)聚低耦合等靈活應(yīng)用這些理論知識(shí)。
到此這篇關(guān)于React封裝CustomSelect組件思路的文章就介紹到這了,更多相關(guān)React封裝CustomSelect組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react中的watch監(jiān)視屬性-useEffect介紹
這篇文章主要介紹了react中的watch監(jiān)視屬性-useEffect使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
React 如何使用時(shí)間戳計(jì)算得到開始和結(jié)束時(shí)間戳
這篇文章主要介紹了React 如何拿時(shí)間戳計(jì)算得到開始和結(jié)束時(shí)間戳,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09
ReactQuery系列之?dāng)?shù)據(jù)轉(zhuǎn)換示例詳解
這篇文章主要為大家介紹了ReactQuery系列之?dāng)?shù)據(jù)轉(zhuǎn)換示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
Rect Intersection判斷兩個(gè)矩形是否相交
這篇文章主要為大家介紹了Rect Intersection判斷兩個(gè)矩形是否相交的算法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06

