React彈窗使用方式NiceModal重新思考
一些有趣的真實(shí)場(chǎng)景
在開(kāi)始進(jìn)入正題之前,先看看一些與彈窗有關(guān)的有趣場(chǎng)景 ??。
案例一:全局彈窗
上圖是掘金的登錄彈窗,未登錄狀態(tài)下觸發(fā)該彈窗展示的時(shí)機(jī)有很多,比如:
- 點(diǎn)擊 Header 上的登錄按鈕
- 文章列表頁(yè)或詳情頁(yè)點(diǎn)贊以及評(píng)論文章
- 發(fā)沸點(diǎn)、評(píng)論沸點(diǎn)以及點(diǎn)贊沸點(diǎn)
- ...
開(kāi)發(fā)者往往會(huì)基于第三方組件庫(kù)定義一個(gè) <LoginModal />
,然后將其掛載至 Root
組件下:
function App() { const [visible, setVisible] = useState(false); const [otherLoginData, setOtherLoginData] = useState(); // other logic ... return ( <div className="app"> <Main /> <LoginModal visible={visible} onCancel={() => setVisible(false)} otherLoginData={otherLoginData} /> </div> ); }
這樣會(huì)帶來(lái)一些問(wèn)題:
<LoginModal />
內(nèi)部邏輯在組件渲染的時(shí)候就會(huì)執(zhí)行,即使彈窗處于隱藏狀態(tài)- 額外的復(fù)雜度。由于存在多個(gè)觸發(fā)時(shí)機(jī),需要將
setVisible
以及setOtherLoginData
透?jìng)髦?<Main />
內(nèi)部的多個(gè)子組件,你可以選擇通過(guò) props 一層一層的傳遞進(jìn)去(鬼知道有多少層?。部梢砸牍ぞ哌M(jìn)行狀態(tài)共享,但不論怎樣,這都不是一件容易的事; - 隨著類(lèi)似的彈窗越來(lái)越多,根組件會(huì)維護(hù)很多彈窗組件的狀態(tài)...天知道為什么展示一個(gè)彈窗需要在多個(gè)文件里反復(fù)橫跳。
展示一個(gè)彈窗,為什么會(huì)變得如此復(fù)雜?
以上案例來(lái)自 @ebay/nice-modal-react,稍作修改
除了上述全局彈窗的場(chǎng)景,還有一種場(chǎng)景也很讓人頭疼。
案例二:存在分支以及依賴(lài)關(guān)系的彈窗
彈窗 1 確認(rèn)彈出彈窗 2,取消則彈出彈窗 3,彈窗 2 以及 彈窗 3 也存在對(duì)應(yīng)的分支邏輯,子孫滿堂輕而易舉,若按照常規(guī)聲明式彈窗組件的實(shí)現(xiàn),非??植溃?/p>
不那么通用的解決方案
擺脫所謂的 React 哲學(xué):數(shù)據(jù)驅(qū)動(dòng)視圖(view = f(data)
),回歸原始的 window.confirm
以及 window.alert
,很多問(wèn)題迎刃而解:
let mountNode: HTMLDivElement | null = null; LoginModal.show = (props?: LoginModalProps) => { if (!mountNode) { mountNode = document.createElement('div'); document.body.appendChild(mountNode); } // 通過(guò) ReactDOM.render 將組件渲染到固定的節(jié)點(diǎn) ReactDOM.render(<LoginModal {...props} visible />, mountNode); }; LoginModal.hide = () => { mountNode && ReactDOM.render(<LoginModal {...props} visible={false} />, mountNode); }; LoginModal.destroy = () => { // 通過(guò) ReactDOM.unmountComponentAtNode 卸載節(jié)點(diǎn) mountNode && ReactDOM.unmountComponentAtNode(mountNode); };
經(jīng)過(guò)以上代碼處理,可以直接通過(guò) LoginModal.show({otherLoginData})
展示彈窗(hide
以及 destroy
同理)。
對(duì)于存在依賴(lài)關(guān)系的彈窗,則可以通過(guò)返回 Promise
進(jìn)行鏈?zhǔn)秸{(diào)用,使用方式如下,此處不過(guò)多展開(kāi)。
try { const ret = await LoginModal.show({ otherLoginData }); const ret = await Ohter1Modal.show(); } catch (error) { const ret = await Other2Modal.show(); }
由于該方案是通過(guò) ReactDOM.render
將組件渲染到一個(gè)新節(jié)點(diǎn),脫離了原始的 React 上下文,在某些強(qiáng)依賴(lài) Context
的場(chǎng)景會(huì)顯得有些麻煩。
可能是最好的彈窗實(shí)踐
某天在逛 github 的時(shí)候發(fā)現(xiàn)了 @ebay/nice-modal-react ,讓我們看看 ebay 的開(kāi)發(fā)者是如何讓彈窗在 React 下變得足夠 Nice。
創(chuàng)建彈窗
import { Modal } from 'antd'; import NiceModal, { useModal } from '@ebay/nice-modal-react'; export default NiceModal.create(({ name }) => { // Use a hook to manage the modal state const modal = useModal(); return ( <Modal title="Hello Antd" onOk={() => modal.hide()} visible={modal.visible} onCancel={() => modal.hide()} afterClose={() => modal.remove()} > Hello {name}! </Modal> ); });
和原先基于 antd 自定義一個(gè)彈窗組件非常相似,只不過(guò)彈窗顯隱相關(guān)的 props
(如 visible/hide/remove
) 是通過(guò) useModal
獲取,最外層則通過(guò) NiceModal.create
將組件進(jìn)行一層封裝(高階組件)。
使用彈窗
在使用彈窗之前,需要在Root
節(jié)點(diǎn)引入 <NiceModal.Provider />
import NiceModal from '@ebay/nice-modal-react'; ReactDOM.render( <React.StrictMode> <NiceModal.Provider> <App /> </NiceModal.Provider> </React.StrictMode>, document.getElementById('root'), );
隨后便可在任意地方通過(guò) NiceModal.show
展示先前自定義的彈窗:
import NiceModal from '@ebay/nice-modal-react'; import MyAntdModal from './my-antd-modal'; // created by above code function App() { const showAntdModal = () => { // Show a modal with arguments passed to the component as props NiceModal.show(MyAntdModal, { name: 'Nate' }) }; return ( <div className="app"> <h1>Nice Modal Examples</h1> <div className="demo-buttons"> <button onClick={showAntdModal}>Antd Modal</button> </div> </div> ); }
以上指引參考自 NiceModal 官方文檔
經(jīng)過(guò) NiceModal
的封裝,好處顯而易見(jiàn):
- 調(diào)用過(guò)程干凈優(yōu)雅
- 組件依舊存在于上下文中(可以自定義位置,默認(rèn)在
Provider
下) show/hide
方法返回值為Promise
,方便鏈?zhǔn)秸{(diào)用(通過(guò)useModal().resolve(val
)等方法改變Promise
狀態(tài))
簡(jiǎn)單實(shí)現(xiàn)思路
如果你對(duì) NiceModal 的實(shí)現(xiàn)原理有興趣,那么可以思考一下我們之前的痛點(diǎn)是什么?案例一中描述了兩大主要痛點(diǎn):
- 根組件渲染彈窗以及維護(hù)彈窗狀態(tài)過(guò)多 ——
NiceModal.Provider
內(nèi)統(tǒng)一渲染彈窗 - 顯隱相關(guān)狀態(tài)以及方法需要共享 —— 庫(kù)內(nèi)部維護(hù)對(duì)應(yīng)狀態(tài),通過(guò)統(tǒng)一 API (show/hide/useModal)暴露
案例二的鏈?zhǔn)秸{(diào)用在脫離聲明式組件后反而變得很容易,利用好 Promise 即可。
const Provider: React.FC = ({ children }) => { // 初始彈窗信息 const [modals, _dispatch] = useReducer(reducer, initialState); dispatch = _dispatch; return ( <NiceModalContext.Provider value={modals}> {children} <NiceModalPlaceholder /> </NiceModalContext.Provider> ); }; const NiceModalPlaceholder: React.FC = () => { const modals = useContext(NiceModalContext); // 根據(jù)彈窗信息找到需要渲染的彈窗 ID (NiceModal.show 時(shí)更新) const visibleModalIds = Object.keys(modals).filter((id) => Boolean(modals[id]) ); // 找到對(duì)應(yīng)創(chuàng)建的高階組件NiceModal.show 時(shí)注冊(cè)),注入 ID const toRender = visibleModalIds .filter((id) => MODAL_REGISTRY[id]) .map((id) => ({ id, ...MODAL_REGISTRY[id], })); return ( <> {toRender.map((t) => ( {/* 渲染 NiceModal.create 創(chuàng)建的高階組件 */} <t.comp key={t.id} id={t.id} /> ))} </> ); }; const create = <P extends Record<string, unknown>>( Comp: React.ComponentType<P> ): React.FC<P & NiceModalHocProps> => ({ id }) => { const modals = useContext(NiceModalContext); const shouldMount = Boolean(modals[id]); if (!shouldMount) { return null; } return ( <NiceModalIdContext.Provider value={id}> {/* 找到對(duì)應(yīng) ID 的參數(shù),傳入內(nèi)部真實(shí)組件 */} <Comp {...(modals[id].args as P)} /> </NiceModalIdContext.Provider> ); };
理解上述三個(gè)方法后,show/hide/useModal 等方法就是基于 dispatch 函數(shù)進(jìn)行彈窗信息 Context 的更新從而觸發(fā)視圖更新(其中 show 方法多一步注冊(cè),生成 ID 并綁定至對(duì)應(yīng)的高階組件)。
推薦閱讀
以上就是React彈窗使用方式NiceModal重新思考的詳細(xì)內(nèi)容,更多關(guān)于React彈窗使用方式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React Hook用法示例詳解(6個(gè)常見(jiàn)hook)
這篇文章主要介紹了React Hook用法詳解(6個(gè)常見(jiàn)hook),本文通過(guò)實(shí)例代碼給大家介紹了6個(gè)常見(jiàn)hook,需要的朋友可以參考下2021-04-04React onClick/onChange傳參(bind綁定)問(wèn)題
這篇文章主要介紹了React onClick/onChange傳參(bind綁定)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02React?Hooks的useState、useRef使用小結(jié)
React Hooks 是 React 16.8 版本引入的新特性,useState和useRef是兩個(gè)常用的Hooks,本文主要介紹了React?Hooks的useState、useRef使用,感興趣的可以了解一下2024-01-01解決React報(bào)錯(cuò)No duplicate props allowed
這篇文章主要為大家介紹了React報(bào)錯(cuò)No duplicate props allowed解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12