Before getting to the main topic, let’s take a look at some interesting scenarios related to pop-ups 🤔.
some interesting real scenes
Case 1: Global pop-up window
The above picture is the login pop-up window of Nuggets. There are many opportunities to trigger the pop-up window display when not logged in, such as:
- Click the login button on the Header
- Likes and comments on the article list page or detail page
- Post boiling, comment boiling, and like boiling
- ...
Developers often define a <LoginModal />
based on a third-party component library, and then mount it under the Root
component:
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>
);
}
This creates some problems:
-
<LoginModal />
The internal logic will be executed when the component is rendered, even if the popup is hidden - extra complexity. Since there are multiple trigger timings, it is necessary to transparently transmit
<Main />
setVisible
andsetOtherLoginData
to multiple sub-components inside ---1da16fdffa204705228c6cc34e4c0243---, you can choose to pass props layer by layer. Pass it in (who knows how many layers!), you can also introduce tools for state sharing, but in any case, this is not an easy task; - As there are more and more similar popup windows, the root component will maintain the state of many popup window components... God knows why displaying a popup window needs to repeatedly jump across multiple files.
Showing a popup, why is it so complicated?
The above case is from @ebay/nice-modal-react, slightly modified
In addition to the above-mentioned global pop-up scenario, there is another scenario that is also very troublesome.
Case 2: Pop-up window with branches and dependencies
A headache for users, a headache for developers
Pop-up 1 confirms pop-up pop-up 2, cancels pop-up pop-up 3, pop-up 2 and pop-up 3 also have corresponding branch logic, and it is easy for children and grandchildren. If it is implemented according to the conventional declarative pop-up component, it is very scary!
Not so generic solution
Get rid of the so-called React Philosophy: Data-Driven Views ( view = f(data)
) and go back to the original window.confirm
and window.alert
, and many problems are solved:
let mountNode: HTMLDivElement | null = null;
LoginModal.show = (props?: LoginModalProps) => {
if (!mountNode) {
mountNode = document.createElement('div');
document.body.appendChild(mountNode);
}
// 通过 ReactDOM.render 将组件渲染到固定的节点
ReactDOM.render(<LoginModal {...props} visible />, mountNode);
};
LoginModal.hide = () => {
mountNode && ReactDOM.render(<LoginModal {...props} visible={false} />, mountNode);
};
LoginModal.destroy = () => {
// 通过 ReactDOM.unmountComponentAtNode 卸载节点
mountNode && ReactDOM.unmountComponentAtNode(mountNode);
};
After the above code processing, you can directly display the pop-up window through LoginModal.show({otherLoginData})
( hide
and destroy
similarly).
For pop-up windows with dependencies, you can make chain calls by returning Promise
. The usage is as follows.
try {
const ret1 = await LoginModal.show({ otherLoginData });
const ret2 = await Ohter1Modal.show();
} catch (error) {
const ret3 = await Other2Modal.show();
}
Since the solution is to render the component to a new node through ReactDOM.render
, which is separated from the original React context, it will be a little troublesome in some scenes with strong dependencies Context
.
Probably the best popup practice
When I was browsing github one day, I found @ebay/nice-modal-react
, let's see how the eBay developers make the pop-up window nice enough under React.
Create a popup
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>
);
});
It is very similar to the original custom pop-up window component based on antd, except that the pop-up window related to display and concealment props
(such as visible/hide/remove
) is obtained through useModal
The outer layer encapsulates the components (higher-order components) through NiceModal.create
.
Use popup
Before using the pop-up window, it needs to be introduced in the Root
node <NiceModal.Provider />
import NiceModal from '@ebay/nice-modal-react';
ReactDOM.render(
<React.StrictMode>
<NiceModal.Provider>
<App />
</NiceModal.Provider>
</React.StrictMode>,
document.getElementById('root'),
);
Then you can display the previously customized popup window anywhere by 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>
);
}
The above guidelines refer to the official documents of NiceModal
After NiceModal
encapsulation, the benefits are obvious:
- The calling process is clean and elegant
- The component still exists in the context (the location can be customized, the default is under
Provider
) -
show/hide
return value of the method isPromise
, which is convenient for chain calls (changed byuseModal().resolve(val)
and other methodsPromise
)
Simple realization idea
If you are interested in the implementation principle of NiceModal, then you can think about what is our previous pain point? Case 1 describes two major pain points:
- The root component renders the pop-up window and maintains the pop-up window state too
NiceModal.Provider
Internal unified rendering of the pop-up window - Explicit and implicit related states and methods need to be shared - the corresponding state is maintained inside the library and exposed through the unified API (
show/hide/useModal
)
The chain call of case 2 becomes very easy after breaking away from the declarative component, just make good use of 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);
// 根据弹窗信息找到需要渲染的弹窗 ID (NiceModal.show 时更新)
const visibleModalIds = Object.keys(modals).filter((id) =>
Boolean(modals[id])
);
// 找到对应创建的高阶组件(NiceModal.show 时注册)
const toRender = visibleModalIds
.filter((id) => MODAL_REGISTRY[id])
.map((id) => ({
id,
...MODAL_REGISTRY[id],
}));
return (
<>
{toRender.map((t) => (
{/* 渲染 NiceModal.create 创建的高阶组件,并注入 ID */}
<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}>
{/* 找到对应 ID 的参数,传入内部真实组件 */}
<Comp {...(modals[id].args as P)} />
</NiceModalIdContext.Provider>
);
};
After understanding the above three methods, show/hide/useModal
and other methods are based on the dispatch
function to update the pop-up data ( Context
) to trigger the view update (where show
method register one more step, generate ID and bind to the corresponding higher-order component).
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。