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ù)的唯一出口,我覺(jué)得應(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-09React 如何使用時(shí)間戳計(jì)算得到開始和結(jié)束時(shí)間戳
這篇文章主要介紹了React 如何拿時(shí)間戳計(jì)算得到開始和結(jié)束時(shí)間戳,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09ReactQuery系列之?dāng)?shù)據(jù)轉(zhuǎn)換示例詳解
這篇文章主要為大家介紹了ReactQuery系列之?dāng)?shù)據(jù)轉(zhuǎn)換示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Rect Intersection判斷兩個(gè)矩形是否相交
這篇文章主要為大家介紹了Rect Intersection判斷兩個(gè)矩形是否相交的算法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06