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

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

