如何在React項目中優(yōu)雅的使用對話框
背景
對話框在前端開發(fā)應用中,是一種非常常用的界面模式。對話框作為一個獨立的窗口,常常被用于信息的展示,輸入信息,亦或者更多其他功能。但是項目的使用過程中,在某些場景下對話框用起來會有一些麻煩。例如:
場景一
如果想要在多個子組件(A、B)中控制一個對話框(C)的顯示影藏,這個對話框必須在共有的父組件(MySalesOrders)中進行聲明。
場景二
如果需要給對話框(C)傳遞參數(shù),一般情況我們會使用 props 傳入,意味著狀態(tài)的管理必須也是子組件(A、B)的父組件或者更高一級進行管理和維護,但是其實這些狀態(tài)可能只需要在子組件 A 或者 B 中維護。這種情況下,我們就需要自定義事件,將狀態(tài)進行回傳,比較麻煩。
const MySalesOrders: React.FC = () => { const [visible, setVisible] = React.useState(false); ... return ( <> <A modalVisible={setVisible}/> <B modalVisible={setVisible}/> { visible ? ( <C ... /> ) : null } </> ); } const A: React.FC = (props) => { ... return ( <> <Button onClick={() => { props.modalVisible(...) }} /> </> ); } const B: React.FC = (props) => { ... return ( <> <Button onClick={() => { props.modalVisible(...) }} /> </> ); }
場景三
一個展示的對話框,對話框在不同的模塊可能只是提示文案不一樣,需要在不同的地方多次導入定義。例如系統(tǒng)中常用的提示成功、提示失敗的對話框。
我們通常會定義一個通用的組件,在父組件中定義,然后使用時喚起,但是如果我們需要在不同的頁面使用,我們就需要在不同的頁面組件中使用引入定義。
這些場景都是在我在實際開發(fā)中都會用到的,并且我們開發(fā)中也是基本都是這樣做的,雖然可以正常的使用。但是隱藏了幾個小的問題。
問題一:難以擴展
如果和 MySalesOrders 同級的組件也要訪問這個對話框(C)?又或者, MySalesOrders 下面的某個深層級的孫子組件也要能對話框(C)?前者意味著代碼需要重構,繼續(xù)提升狀態(tài)到 MySalesOrders 組件的父組件;后者意味著業(yè)務邏輯處理更復雜,需要通過層層的自定義事件回調來完成。
問題二:維護問題
同一個組件,需要在不同的地方多次的導入定義。在系統(tǒng)中增加了大量重復的代碼。代碼很快就會變得臃腫,且難以理解和維護。
問題的本質
對上訴問題來說,本質在于:在我們日常的項目中應該哪里定義去對話框?又該如何和對話框進行數(shù)據(jù)交互?
對話框的本質
換一個角度再來看對話框,其實對話框本身是一個一對一或者一對多的 UI 模式。站在對話框的角度上,對話框本質上是一個「獨立于其他界面的一個窗口,用于完成一個獨立的功能」。
如果從視覺角度出發(fā),你會發(fā)現(xiàn)在使用對話框的時候,你完全不會關心它是從哪個具體的組件中彈出來的,而只會關心對框本身的內容。比如說,成功和失敗的對話框,它可能在 A 組件點出來的,也可能是 B 組件點出來的,亦或者其他組件點出來的。對話框的本質就決定了它是獨立于各個組件之外的,
雖然很可能在一開始這個對話框的實現(xiàn)和某個組件非常高的相關度,但是在整個應用的不斷開發(fā)和演進過程中,是很可能不斷變化的。所以,在定義一個對話框的時候,其定位基本會等價于定義一個具有唯一 URL 路徑的頁面。只是前者由彈出層實現(xiàn),后者是頁面的切換。對于頁面級別的 UI 切換,我們很容易理解,就是定義全局的路由嘛。那么同樣的,如果我們以同樣的方式去思考對話框,其實就是將對話框全局化,然后通過一個全局的機制來管理這些對話框。這個過程和頁面 URL 的切換非常類似,那么我們就可以給每一個對話框定義一個全局唯一的 ID,然后通過這個 ID 去顯示或者隱藏一個對話框,并且給它傳遞參數(shù)。
基于這樣的設想,我們可以嘗試使用全局的狀態(tài)管理來設置我們的對話框。
全局的狀態(tài)管理的對話框
整體的架構
具體實現(xiàn)
代碼實現(xiàn)以 React 項目為主。
Redux - reducer 存儲
利用 Redux 的 store 去存儲每個對話框狀態(tài)和參數(shù)。
export default (state = { hiding: {} }, action: AnyAction) => { switch (action.type) { case CONSTANTS.modalShow: return { ...state, [action.payload.modalId]: action.payload.args || true, hiding: { ...state.hiding, [action.payload.modalId]: false, }, }; case CONSTANTS.modalHide: return action.payload.force ? { ...state, [action.payload.modalId]: false, hiding: { [action.payload.modalId]: false }, } : { ...state, hiding: { [action.payload.modalId]: true } }; default: return state; } };
Redux - action 處理對話框的顯示隱藏
兩個 action ,分別用來顯示和隱藏對話框。
export function showModal(modalId: string, args: any) { return { type: CONSTANTS.modalShow, payload: { modalId, args, }, }; } export function hideModal(modalId: string, force: any) { return { type: CONSTANTS.modalHide, payload: { modalId, force, }, }; }
Hook - useCommonModal
定義一個 Hook,在其內部封裝對 Store 的操作,從而實現(xiàn)對話框狀態(tài)管理的邏輯重用。
export const useCommonModal = (modalId: string) => { const dispatch = useDispatch(); const show = React.useCallback( (args?: any) => new Promise((resolve) => { commonmModalCallbacks[modalId] = resolve; dispatch(showModal(modalId, { ...args })); }), [dispatch, modalId], ); const resolve = React.useCallback( (args?: any) => { if (commonmModalCallbacks[modalId]) { commonmModalCallbacks[modalId]({ ...args }); delete commonmModalCallbacks[modalId]; } }, [modalId], ); const hide = React.useCallback( (force?: any) => { dispatch(hideModal(modalId, force)); delete commonmModalCallbacks[modalId]; }, [dispatch, modalId], ); const args = useSelector((s: any) => s?.modalReducer?.[modalId]); const hiding = useSelector((s: any) => s?.modalReducer?.hiding?.[modalId]); return React.useMemo( () => ({ args, hiding, visible: !!args, show, hide, resolve }), [args, hide, show, resolve, hiding], ); };
創(chuàng)建對話框-容器模塊
創(chuàng)建對話框時,使用容器模式,它會在對話框不可見時直接返回 null,從而不渲染任何內容;并且確保即使頁面上定義了 100 個對話框,也不會影響頁面性能。
export const createCommonModal = (modalId: string, Comp: any) => (props: any) => { const { visible, args } = useCommonModal(modalId); if (!visible) return null; return ( <Comp {...args} {...props} /> ); };
對話框返回值處理
往往在實際的使用中,可能在打開對話框進行操作之后需要將返回值返給調用者,有兩種方式可以供參考:
- callback:在傳入?yún)?shù)時,傳入一個回調函數(shù),在進行操作完成之后,進行回調函數(shù)的調用。
const show = React.useCallback( (args?: any) => new Promise((resolve) => { commonmModalCallbacks[modalId] = resolve; // args 中攜帶上 callback dispatch(showModal(modalId, { ...args })); }), [dispatch, modalId], ); // 調用 const modal = useCommonModal('modal-id'); modal.show({ callback() {} }); // 對話框解析參數(shù) const modalReducer = useSelector((state: any) => state.modalReducer); const { callback } = modalReducer?.['modal-id']; //對話框觸發(fā) callback();
- 將 show 和 resolve 兩個函數(shù)通過 Promise 聯(lián)系起來。通過臨時變量,來存放 resolve 回調函數(shù),在對話框中去調用 modal.resolve 來進行值的返回。
const resolve = React.useCallback( (args?: any) => { if (commonmModalCallbacks[modalId]) { commonmModalCallbacks[modalId]({ ...args }); delete commonmModalCallbacks[modalId]; } }, [modalId], ); // 調用 const modal = useCommonModal('modal-id'); modal.show(args).then(result => {}); // 對話框觸發(fā) const modal = useCommonModal('modal-id'); modal.resolve({ ... });
運行實例
總結
分享了一種使用對話框的實踐方式:利用全局狀態(tài)來管理對話框。解決上文提到的在使用對話框遇到的問題。其核心思路在于從 UI 模式的角度出發(fā),把對話框也可當做一個單獨的頁面,對話框的展示可用全局狀態(tài)來管理,因此,用全局的方式去管理對話框就是一種非常合理的方式。從而讓組件的語義更加清楚,代碼更容易理解和維護。
并且對于對話框定義位置,其實可以分場景來甄別。系統(tǒng)某一個模塊下的業(yè)務對話框,就只需要定義在這個業(yè)務模塊的根組件下就可以了。對于全局都可能使用的公共對話框,那就可以定義在整個系統(tǒng)的根組件,系統(tǒng)任何地方都可以使用。定義的位置決定了對話框組件輻射的廣度。
當然這種全局的狀態(tài)管理對話框的方式,只是對原有的對話框操作做了一個增強,解決了一些場景下的問題,但是對于一些簡單的對話框我們還是可以用常用的方式去管理和控制。兩者是可以并存的,大家可以根據(jù)場景來定義使用哪一種方式。
參考
- http://chabaoo.cn/article/247814.htm
- time.geekbang.org/column/arti…
- http://chabaoo.cn/article/247817.htm
- ant.design/components/…
- www.chkui.com/article/rea…
到此這篇關于如何在React項目中優(yōu)雅的使用對話框的文章就介紹到這了,更多相關React使用對話框內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Mobx實現(xiàn)React?應用的狀態(tài)管理詳解
這篇文章主要為大家介紹了Mobx?實現(xiàn)?React?應用的狀態(tài)管理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12React onClick/onChange傳參(bind綁定)問題
這篇文章主要介紹了React onClick/onChange傳參(bind綁定)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02react-player實現(xiàn)視頻播放與自定義進度條效果
本篇文章通過完整的代碼給大家介紹了react-player實現(xiàn)視頻播放與自定義進度條效果,代碼簡單易懂,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2022-01-01React使用context進行跨級組件數(shù)據(jù)傳遞
這篇文章給大家介紹了React使用context進行跨級組件數(shù)據(jù)傳遞的方法步驟,文中通過代碼示例給大家介紹的非常詳細,對大家學習React context組件數(shù)據(jù)傳遞有一定的幫助,感興趣的小伙伴跟著小編一起來學習吧2024-01-01通過React-Native實現(xiàn)自定義橫向滑動進度條的 ScrollView組件
開發(fā)一個首頁擺放菜單入口的ScrollView可滑動組件,允許自定義橫向滑動進度條,且內部渲染的菜單內容支持自定義展示的行數(shù)和列數(shù),在內容超出屏幕后,渲染順序為縱向由上至下依次排列,對React Native橫向滑動進度條相關知識感興趣的朋友一起看看吧2024-02-02