This package from 🇷🇺 Russia with love!
You can read this readme in Russian.
- Components in the popup have a domain area environment
- No need for a centralized popup store
- Popups can be opened from anywhere in the application
- Simple API
- Can be used outside of React (e.g., in STM)
- Microfrontends support
This package does not implement the UI of modal windows. It is only intended to manage them in the application.
You can use it with any UI popups in React, such as modal windows from Material-UI, Ant Design, react-modal, or any others.
Popup - A UI component with content that can be shown or hidden depending on the value of a certain props variable.
Popups themselves do not contain the business logic of the application.
Popups can be divided into dynamic and static, as well as local and global.
Dynamic popup - The content of such a popup is mounted and unmounted only when the popup is opened and closed.
Static popup - The content of such a popup is mounted and unmounted along with the domain area in which it will be used.
In such components, it does not make sense to use useEffect
on mount, most likely this hook will trigger long before the popup is opened.
As a rule, such popups retain their state between openings, which can be useful in some tasks.
Actions - components that are containers for popups.
In the simplest case, they contain the business logic of the application, needed only to open and close popups.
example:
import type { FC, PropsWithChildren } from "react";
import { usePopup } from "react-use-popup";
const ExampleAction: FC<PropsWithChildren> = props => {
const { children } = props;
const visible = usePopup("popup-example");
return (
<Popup visible={visible}>
{children}
</Popup>
);
};
You can place actions anywhere in the application where it is convenient for you.
It is recommended to do this in the domain area where the popup will be used, as you will be able to use props, contexts, and hooks from this area.
Not all popups (rather almost none) should be completely global.
Local popups - popups that open only in one specific place in the application. An action with such a popup is conveniently placed directly in the component where it will be used.
example:
<>
<Button onClick={() => openPopup("popup-example")}>Open popup</Button>
<ExampleAction>local popup content</ExampleAction>
</>
Global popups - popups that can be opened from anywhere in a specific domain area of the application. An action with such a popup is conveniently placed in the root component of the domain area.
As a rule, these are popups that can be opened from different places in the application.
<>
<...>
<Button onClick={() => openPopup("popup-example")}>Open popup</Button>
</...>
<...>
<Button onClick={() => openPopup("popup-example")}>Open popup</Button>
</...>
<ExampleAction>global popup content</ExampleAction>
</>
At the same time, if the user leaves the domain area, the popup will be unmounted.
For each action, you need to create a unique intent: string
- a key to open the popup with this action.
import type { FC, PropsWithChildren } from "react";
import { usePopup } from "react-use-popup";
// It is convenient to describe the intent in the action component and export it from there
export const intent = "popup-example";
const ExampleAction: FC<PropsWithChildren> = props => {
const { children } = props;
// use the intent to get the popup open state
const visible = usePopup(intent);
return (
<Popup visible={visible}>
{children}
</Popup>
);
};
To open a popup, just call the open method with the desired intent
.
import { openPopup } from "react-use-popup";
import { intent } from "./ExampleAction";
...
<Button onClick={() => openPopup(intent)}>Open popup</Button>
This works both in React and outside of it (redux, effector, sagas, etc.)
Why?
CustomEvent
is used to manage popups. The context iswindow
, which is available everywhere.
To close a popup, you need to call the close method with the same intent
.
import { closePopup } from "react-use-popup";
import { intent } from "./ExampleAction";
...
<Button onClick={() => closePopup(intent)}>Close popup</Button>
You can also set handlers that will be called when the popup is opened/closed.
Just pass them as props. Or use contexts, hooks, etc. from your domain area.
You can use
useEffect
on props as usual.
import type { UUID } from "node:crypto";
import type { FC } from "react";
import { useParams } from "react-router";
import { usePopup } from "react-use-popup";
export const intent = "popup-example";
const ExampleAction: FC = props => {
const { articleId } = useParams() as { articleId: UUID };
const visible = usePopup(intent);
return (
<Popup visible={visible}>
<Article id={articleId} />
</Popup>
);
};
In dynamic popups, it is convenient to use useEffect
on mount.
const Article: FC<{ id: UUID }> = props => {
const { id } = props;
useEffect(() => {
fetchArticle(id);
}, []);
return <div>Article content</div>;
};
This is relevant for static popups. In dynamic popups, it is probably easier to use
useEffect
on mount (see above).
You can pass an object with parameters to the popup open method.
import { openPopup } from "react-use-popup";
import { intent } from "./ExampleAction";
...
const openHandler = useCallback(
() => openPopup(intent, { userId }),
[userId]
);
...
<Button onClick={openHandler}>Open popup</Button>
These parameters will be passed to the open handler and you will be able to process them.
const ExampleAction: FC = () => {
const visible = usePopup(intent, {
open: ({ userId }) => sendAnalytics("popup opened", intent, userId)
});
return (
<Popup visible={visible}>
...
</Popup>
);
};
In this case, it is not recommended to change the props of the component in the popup.
It is better to call the method from the component directly (see below).
The idea is that the logic of the component inside the popup does not know that it is in a popup.
However, if we cannot use useEffect
on mount (for example, in static popups), then we can pass control outside (preferably using ref / useImperativeHandle
).
const ExampleAction: FC = () => {
const ref = useRef(null);
const visible = usePopup(intent, {
open: ({ userId }) => ref.current?.loadData(userId)
});
return (
<Popup visible={visible}>
<PopupContent ref={ref} />
</Popup>
);
};
This avoids unnecessary re-renders and also allows exporting additional methods.
const PopupContent = props => {
const { ref } = props;
const [data, setData] = useState(null);
const loadData = useCallback(async (userId: UUID) => {
const data = await fetchData(userId);
setData(data);
}, []);
useImperativeHandle(ref, () => ({ loadData }), [loadData]);
return <div>{data}</div>;
};
- The form in the popup manages the logic of sending data to the server itself, and to close the popup after a successful submission, you need to call the popup close method. To do this, pass it to the form.
- And from the form, export the submit controller and attach it to the button in the popup.
import { closePopup, usePopup } from "react-use-popup";
export const intent = "popup-example";
const closeHandler = () => closePopup(intent);
const ExampleAction: FC = () => {
const ref = useRef(null);
const visible = usePopup(intent);
return (
<Popup visible={visible}>
<PopupContent closePopup={closeHandler} ref={ref} />
<SubmitButton onClick={() => ref.current?.sendForm()} />
</Popup>
);
};
Inside the component that will be in the popup, we describe the logic of submission, as we are directly in the business logic of the application.
const PopupContent = props => {
const { closeHandler, ref } = props;
const sendForm = useCallback(async (userId: UUID) => {
try {
await sendFormData(userId);
closeHandler();
} catch (error) {
console.error(error);
}
}, [closeHandler]);
useImperativeHandle(ref, () => ({ sendForm }), [sendForm]);
return <form>...</form>;
};
Here the form manages the logic of sending itself to the server.
- if there is an error - show the error
- if successful - then after sending, close the popup
Just create another action specifically for confirmation (maybe you can even make it universal).
Now just open the new action on top of the old one and pass it the confirm form handler.
const { pathname } = useLocation();
useEffect(() => {
if (!pathname.endsWith("/popup")) return;
openPopup(intent);
}, [pathname]);
Changing the URL when opening a popup does not make sense - it is better to just change the URL + use the code above → the behavior will be the same.
This is when the same popup is open, but several windows are open at the same time, and the content is different.
This can be resolved at the Action level.
The only thing is that since the history is custom, you will need to not use the usePopup
hook, but make the handlers yourself - they should create an instance of the popup and add it to the list that will be rendered in this Action.
issue: Multi-instance support
$ npm install react-use-popup
openPopup<OpenParams>(intent: string, detail?: OpenParams): void
opens a popup with the specified intent
and passes parameters to the open handler.
closePopup<CloseParams>(intent: string, detail?: CloseParams): void
closes a popup with the specified intent
and passes parameters to the close handler.
usePopup(intent: string, hooks?: UsePopupHooks<OpenParams, CloseParams>): boolean
returns the open state of the popup with the specified intent
and allows you to set open and close handlers.
type UsePopupHooks<OpenParams, CloseParams> = {
open?: (detail: OpenParams) => void;
close?: (detail: CloseParams) => void;
}