前提:一個通過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) => (</p> <p> <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有很大的考慮余地,這是整個組件設計的根本要素之一,至于后續傳導什么參數,是否好用都是在再考量。
還有一個要點就是數據流: 組件內部的數據流如何清晰而方便的控制,也要考量如何與外層調用組件交互,這樣就直接決定了組件的復雜度。
經驗分享:比如復雜的核心方法在里面可以使用柯里化根據參數重要性分層傳入;對于復雜的多類別的子組件看使用分發模式解耦;簡單些的考慮用高內聚低耦合等靈活應用這些理論知識。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/127736.html
摘要:創建一個普通函數因為的存在所以變成構造函數創建一個方法在方法中,創建一個中間實例對中間實例經過邏輯處理之后返回使用方法創建實例而恰好,高階組件的創建邏輯與使用,與這里的方法完全一致。因為方法其實就是構造函數的高階組件。 很多人寫文章喜歡把問題復雜化,因此當我學習高階組件的時候,查閱到的很多文章都給人一種高階組件高深莫測的感覺。但是事實上卻未必。 有一個詞叫做封裝。相信寫代碼這么久了,大...
摘要:或者兄弟組件之間想要共享某些數據,也不是很方便傳遞獲取等。后面要講到的就是通過讓各個子組件拿到中的數據的。所以,確實和沒有什么本質關系,可以結合其他庫正常使用。 本文介紹了react、redux、react-redux之間的關系,分享給大家,也給自己留個筆記,具體如下: React 一些小型項目,只使用 React 完全夠用了,數據管理使用props、state即可,那什么時候需要引入...
摘要:前面有講到過很多頁面會在初始時驗證登錄狀態與用戶角色。這個時候就涉及到一個高階組件的嵌套使用。而每一個高階組件函數執行之后中所返回的組件,剛好可以作為下一個高階組件的參數繼續執行,而并不會影響基礎組件中所獲得的新能力。 前面有講到過很多頁面會在初始時驗證登錄狀態與用戶角色。我們可以使用高階組件來封裝這部分驗證邏輯。封裝好之后我們在使用的時候就可以如下: export default w...
閱讀 547·2023-03-27 18:33
閱讀 732·2023-03-26 17:27
閱讀 630·2023-03-26 17:14
閱讀 591·2023-03-17 21:13
閱讀 521·2023-03-17 08:28
閱讀 1801·2023-02-27 22:32
閱讀 1292·2023-02-27 22:27
閱讀 2178·2023-01-20 08:28