A lightweight modal manager for React.
It allows you to easily manage modals in your react app in a imperative way.
Using modals in React is often a bit exhausting. Imagine there's a button in the view and we need to open a modal when the button is clicked.
Typically, we need to do something like this:
- first create a modal component.
- then create a state that controls the display of the modal where the modal is used and pass it to the modal.
- finally, open the modal by updating this state.
When there are a lot of modals in a view, we need to manage a lot of state to control the modals.
Even worse, it may be enough to open a modal when a button is clicked, but as requirements change and you need to open the same modal from different places, you have to rethink where the modal and corresponding state should be declared.
To avoid these agonizing situations, I created this library to try to solve these problems, and I hope it will help you too.
Note: This library is not a React modal component, it just provides the API for modal management, so you should use it with other modal components (Modal / Dialog provided by UI libraries like Material UI, Ant Design, etc).
- Lightweight: Zero dependency and small
- Uncontrolled: Manage modals from anywhere, even outside of React
- Promise API: Can use Promise to interact with the outside of the modal
- Props Binding: Easy to pass props and keep state up-to-date.
- Platform Agnostic: No platform binding, can be used in any React environment in theory
- UI Agnostic: Easy to integrate with other UI libraries
# with npm
npm install @whaoa-libs/react-modal-manager
# or with pnpm
pnpm add @whaoa-libs/react-modal-manager
# or with yarn
yarn add @whaoa-libs/react-modal-manager
First, you need to create a ModalManager instance through createModalManager
,
and all management operations will be handled through this instance.
You can also create as many instances as you want, each instance is independent.
Then, you need to wrap your modal component with createModal
,
it accepts a component that can read the current modal's state and API via useModal
.
// @filename: './modal.tsx'
import { createModal, createModalManager } from '@whaoa-libs/react-modal-manager';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
// create modal manager
// eslint-disable-next-line react-refresh/only-export-components
export const mm = createModalManager();
// wrapper your modal component with createModal
export const Modal = createModal((props: { content?: string }) => {
const { content } = props;
const state = useModal();
return (
<Dialog open={state.visible} onClose={state.close}>
<DialogTitle>MUI Dialog</DialogTitle>
<DialogContent dividers>
{content || 'default modal content'}
</DialogContent>
<DialogActions>
<Button autoFocus onClick={state.close}>Close</Button>
</DialogActions>
</Dialog>
);
});
Before using the ModalManager, you need to make sure that the ModalStackPlacement
is rendered.
ModalStackPlacement
is used to render modal components,
the modal opened by ModalManager will be eventually rendered by ModalStackPlacement
.
I recommend that you place it at the top of your application, you can also render it anywhere else,
but make sure that only one ModalStackPlacement
component is rendered for the same ModalManager,
otherwise multiple modals may be rendered at the same time!
Then, you can open a modal via ModalManager.open
. The open
method takes two arguments,
the first is the modal component wrapped by createModal
,
and the second (optional) is the props passed to the component.
PS: You can call ModalManager.open
from anywhere, including outside of React.
import { ModalStackPlacement } from '@whaoa-libs/react-modal-manager';
import { Modal, mm } from './modal';
function App() {
return (
<button type="button" onClick={() => mm.open(Modal, { content: 'modal content' })}>
open modal
</button>
);
}
export function Root() {
return (
<div>
<App />
<ModalStackPlacement modalManager={mm} />
</div>
);
};
In addition to passing functions like onClose to the modal as props,
you can also handle modal close events with a Promise via the return value of ModalManager.open
.
You can close or remove a modal in these ways:
- inside a modal via the
close
andremove
methods returned byuseModal
. - outside the modal via the
close
andremove
methods on the modal instance returned byModalManager.open
. - via the
close
andremove
methods on theModalManager
(need to pass the id on the modal instance returned byModalManager.open
).
When calling close
and remove
, you can pass an argument as a payload,
which can be accessed in the then callback of the modal's Promise instance via result.payload
,
and you can distinguish between close
and remove
via result.type
.
The ModalManager.open
method returns a modal instance
with a promise
property that is a Promise instance indicating when the modal is closed,
when the modal is closed or removed, the Promise is marked as resolved.
import { Modal, mm } from './modal';
const modal = mm.open(Modal);
modal.promise.then((result) => {
switch (result.type) {
case 'close':
// payload of close event
console.log(result.payload);
break;
case 'remove':
// payload of remove event
console.log(result.payload);
break;
}
});
Note: Since the state of a Promise can be marked only once,
only the first call to close
or remove
will change the state of the Promise for the modal after it is opened.
This means that if close
is called first and then remove
is called,
then remove
will not change the Promise.
Sometimes you need to pass state that changes in React to a modal,
but ModalManager.open
can't do that, so you need to use the ModalController
component to do it.
ModalController
is a React component that accepts a modal created by createModal
as a prop
and passes any other props it receives to the modal component.
The modal rendered by ModalController
component is controlled by the internal ModalManager,
and can be opened and closed via the component's ref.
import { useEffect, useRef, useState } from 'react';
import { ModalController } from '@whaoa-libs/react-modal-manager';
import { Modal } from './modal';
import type { ModalControllerRef } from '@whaoa-libs/react-modal-manager';
export function App() {
const modalRef = useRef<ModalControllerRef<{ content?: string }>>();
const [datetime, setDatetime] = useState('');
useEffect(() => {
const intervalId = setInterval(() => setDatetime(new Date().toUTCString()), 1000);
return () => clearInterval(intervalId);
}, []);
return (
<div>
<button type="button" onClick={() => modalRef.current?.open()}>open modal</button>
<ModalController ref={modalRef} modal={Modal} content={datetime} />
</div>
);
}
Note: Because of the different rendering mode, the ref does not provide a remove
method.
useModal.remove
will fallback to the close
method.
MIT © React Modal Manager