nice-modal-react 源码解析

本文最后更新于: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 }) => {
// hooks 管理当前 Modal 状态
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" />
// 或通过注册API
NiceModal.register('/user/info/edit', UserInfoModal)
// 或通过modaldef
<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 });
//或使用React Hook
const modal = NiceModal.useModal(UserInfoModal);
//通过ID使用它
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);

// If there's modal state, then should mount it.
const modals = useContext(NiceModalContext);
const shouldMount = !!modals[id];

useEffect(() => {
// If defaultVisible, show it after mounted.
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;
// If modal.show is called
// 1. If modal was mounted, should make it visible directly
// 2. If modal has not been mounted, should mount it first, then make it visible
useEffect(() => {
if (delayVisible) {
// delayVisible: false => true, it means the modal.show() is called, should show it.
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>>;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
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;
// If use a component directly, register it.
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,非常的巧妙,值得学习。