本文最后更新于:2022年7月13日 凌晨
最近在项目上尝试了 eBay/nice-modal-react 来处理态框,相对于我之前使用 Modal 的方式更加优雅。它是一个零依赖,小巧的模态框管理库,实现思路让我眼前一亮,值得学习。
概述 我们先来看看 nice-modal-react 的使用。
可以将它的使用分为三步,创建 Modal、注册 Modal 和使用 Modal。
创建 Modal 通过 NiceModal.create
高阶函数来创建可使用的 Modal,同时在内部使用 useModal
来管理当前 Modal 的相关状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Modal } from 'antd' ;import NiceModal , { useModal } from '@ebay/nice-modal-react' ;const UserInfoModal = NiceModal .create (({ name } ) => { const modal = useModal (); return ( <Modal title ="Hello Antd" onOk ={() => modal.hide()} onCancel={() => modal.hide()} afterClose={() => { modal.hideResolve(); modal.remove(); }} > Hello ${name}! </Modal > ); });export default UserInfoModal ;
注册 Modal 有三种方式注册我们的 Modal。
1 2 3 4 5 6 <UserInfoModal id="/user/info/edit" />NiceModal .register ('/user/info/edit' , UserInfoModal ) <ModalDef id='/user/info/edit' component={UserInfoModal } />
当然这一步是可选的,不注册直接使用情况下,它会自动帮我们注册成全局 Modal。
接下来我们可以在任意地方使用它。
使用 Modal 首先,我们需要将加入全局上下文NiceModal.Provider
:
1 2 3 4 5 6 7 8 9 import NiceModal from '@ebay/nice-modal-react' ;ReactDOM .render ( <React.StrictMode > <NiceModal.Provider > <App /> </NiceModal.Provider > </React.StrictMode > , document .getElementById ('root' ) );
然后我们就可以通过 hooks 来解决各种 Modal 业务场景了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import NiceModal , { useModal } from '@ebay/nice-modal-react' ;import UserInfoModal from './UserInfoModal' ;NiceModal .show (UserInfoModal , { userId : 666 });const modal = NiceModal .useModal (UserInfoModal );const modal = NiceModal .useModal ('/user/info/edit' ); modal.show ({ userId : 666 }); modal.show ({ userId : 666 }).then (refreshUserList);await modal.hide ();
源码 接下来我们看看它的源码,核心文件只有一个 index.tsx ,总共 500 多行,非常小巧。
创建 我们先来看看创建, NiceModal.create
是一个高阶组件,它其实做的是根据 ModalId 从 Context 中获取当前 Modal 状态。如果当前 ModalId 不再 Context 中就不渲染当前 Modal,如果存在就将相关状态(props)和参数(args)传入对应 Modal。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 export const create = <P extends {}>(Comp : React .ComponentType <P>): React .FC <P & NiceModalHocProps > => { return ({ defaultVisible, keepMounted, id, ...props } ) => { const { args, show } = useModal (id); const modals = useContext (NiceModalContext ); const shouldMount = !!modals[id]; useEffect (() => { if (defaultVisible) { show (); } ALREADY_MOUNTED [id] = true ; return () => { delete ALREADY_MOUNTED [id]; }; }, [id, show, defaultVisible]); useEffect (() => { if (keepMounted) setFlags (id, { keepMounted : true }); }, [id, keepMounted]); const delayVisible = modals[id]?.delayVisible ; useEffect (() => { if (delayVisible) { show (args); } }, [delayVisible, args, show]); if (!shouldMount) return null ; return ( <NiceModalIdContext.Provider value ={id} > <Comp {... (props as P )} {...args } /> </NiceModalIdContext.Provider > ); }; };
注册 这里非常简单,就是加入一个全局变量(MODAL_REGISTRY
)中,之后都将在 placeholder 中呈现。
1 2 3 4 5 6 7 export const register = <T extends React .FC <any >>(id : string , comp : T, props?: NiceModalArgs <T>): void => { if (!MODAL_REGISTRY [id]) { MODAL_REGISTRY [id] = { comp, props }; } else { MODAL_REGISTRY [id].props = props; } };
使用 Provider 提供注册的 Modal 上下文,这里有一个非常核心的地方就是NiceModalPlaceholder
,在这里实现所需要 Modal 的渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export const Provider : React .FC <Record <string , unknown >> = ({ children, dispatch: givenDispatch, modals: givenModals, }: { children: ReactNode; dispatch?: React.Dispatch<NiceModalAction>; modals?: NiceModalStore; } ) => { return ( <NiceModalContext.Provider value ={givenModals} > {children} <NiceModalPlaceholder /> </NiceModalContext.Provider > ); };
NiceModalPlaceholder
的实现非常简单,从 context 中获取需要展示的 ModalId,同时从 MODAL_REGISTRY 中获取 Modal 信息,过滤后进行渲染。
当我们调用modal.show()
时,会添加 Modal 信息,就会渲染对应 Modal。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const NiceModalPlaceholder : React .FC = () => { const modals = useContext (NiceModalContext ); const toRender = visibleModalIds .filter ((id ) => MODAL_REGISTRY [id]) .map ((id ) => ({ id, ...MODAL_REGISTRY [id], })); return ( <> {toRender.map((t) => ( <t.comp key ={t.id} id ={t.id} {...t.props } /> ))} </> ); };
useModal
从 context 中获取 ModalId 对应的状态和参数,如果不存在当前 ModalId,就注册一个。同时返回对应的 props。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 export function useModal<T extends React .FC <any >>( modal : T, args?: NiceModalArgs <T> ): NiceModalHandler <NiceModalArgs <T>>;export function useModal (modal?: any , args?: any ): any { const modals = useContext (NiceModalContext ); const contextModalId = useContext (NiceModalIdContext ); let modalId : string | null = null ; const isUseComponent = modal && typeof modal !== 'string' ; if (!modal) { modalId = contextModalId; } else { modalId = getModalId (modal); } const mid = modalId as string ; useEffect (() => { if (isUseComponent && !MODAL_REGISTRY [mid]) { register (mid, modal as React .FC , args); } }, [isUseComponent, mid, modal, args]); return { id : mid, args : modalInfo?.args , visible : !!modalInfo?.visible , keepMounted : !!modalInfo?.keepMounted , show : showCallback, hide : hideCallback, remove : removeCallback, resolve : resolveCallback, reject : rejectCallback, resolveHide, }; }
这里有个细节就是 getModalId
会在每个 Modal 组件上写入一个 SymbolId,也就是说组件即使重复注册,会使用同一个 Id。
1 2 3 4 5 6 7 const getModalId = (modal : string | React .FC <any >): string => { if (typeof modal === 'string' ) return modal as string ; if (!modal[symModalId]) { modal[symModalId] = getUid (); } return modal[symModalId]; };
三方组件库支持 它对于第三方组件也有支持,实现非常简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export const antdDrawer = ( modal : NiceModalHandler ): { visible : boolean ; onClose : () => void ; afterVisibleChange : (visible: boolean ) => void } => { return { visible : modal.visible , onClose : () => modal.hide (), afterVisibleChange : (v: boolean ) => { if (!v) { modal.resolveHide (); } !v && !modal.keepMounted && modal.remove (); }, }; };
总结 nice-modal-react 设计上的核心是 NiceModalPlaceholder
,通过它来灵活调用 Modal,非常的巧妙,值得学习。