react-use-popup v1.0.3
Popups management for React SPA
You can read this readme in English.
Motivation / Features
- Компоненты в попапе имеют окружение доменной области
- Не хочется иметь централизованное хранилище попапов
- Можно открывать попапы из любого места приложения
- Простая API
- Можно использовать вне реакта (например, в STM)
- Поддержка microfrontends
Этот пакет не реализует UI модальных окон. Он предназначен только для управления ими в приложении
Вы можете использовать его с любыми UI-попапами в React, например, с модальными окнами из Material-UI, Ant Design, react-modal или любыми другими
Little bit of theory / Vocabulary
Попап - UI-компонент с контентом, который может быть показан либо скрыт в зависимости от значения некой props-переменной.
Сами попапы не содержат в себе бизнес-логику приложения.
Попапы можно разделить на динамические и статическиме, а также на локальные и глобальные.
Динамический попап - Содержимое такого попапа маунтится и анмаунтится только при открытии и закрытии попапа.
Статический попап - Содержимое такого попапа маунтится и анмаунтится вместе с доменной областью в которой будет использоваться.
В таких компонентах не имеет смысла использовать useEffect
на маунт, скорее всего этот хук сработает задолго до открытия попапа.
Как правило, такие попапы сохраняют состояния между открытиями, что может быть полезно в некоторых задачах
Экшены - компоненты которые являются контейнерами для попапов.
В самом простом случае они содержат бизнес-логику приложения, нужны только для открытия и закрытия попапов.
пример:
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>
);
};
Вы можете располагать экшены в любом месте приложения, где вам удобно.
Рекомендуется делать это в доменной области, где будет использоваться попап, так как вы сможете использовать пропсы, контексты и хуки из этой области.
Далеко не все попапы (скорее практически никакие) не должны быть прям совсем глобальными.
Локальные попапы - попапы, которые открываются только в одном конкретном месте приложения. Экшн с таким попапом удобно располагать прямо в компоненте, где он будет использоваться.
<>
<Button onClick={() => openPopup("popup-example")}>Open popup</Button>
<ExampleAction>local popup content</ExampleAction>
</>
Глобальные попапы - попапы, которые могут быть открыты из любого места конкретной доменной области приложения. Экшн с таким попапом удобно располагать в корневом компоненте доменной области.
Как правило это попапы, которые могут быть открыты из разных мест приложения
<>
<...>
<Button onClick={() => openPopup("popup-example")}>Open popup</Button>
</...>
<...>
<Button onClick={() => openPopup("popup-example")}>Open popup</Button>
</...>
<ExampleAction>global popup content</ExampleAction>
</>
При этом если пользователь покинет доменную область, попап будет размонтирован.
Usage
Для каждого экшена нужно завести уникальный intent: srting
- ключ для открытия попапа с этим экшеном
import type { FC, PropsWithChildren } from "react";
import { usePopup } from "react-use-popup";
// Удобно описывать intent в компоненте экшена и экспортировать из него
export const intent = "popup-example";
const ExampleAction: FC<PropsWithChildren> = props => {
const { children } = props;
// использование intent для получения состояния открытия попапа
const visible = usePopup(intent);
return (
<Popup visible={visible}>
{children}
</Popup>
);
};
Для открытия попапа надо просто вызвать метод открытия с нужным intent
.
import { openPopup } from "react-use-popup";
import { intent } from "./ExampleAction";
...
<Button onClick={() => openPopup(intent)}>Open popup</Button>
Это работает и в реакте и за его пределами (redux, эффектор, саги и т.д.)
Почему? Для управления поапами используется
CustomEvent
. Контекстом выступаетwindow
, который доступен везде.
Для закрытия попапа нужно вызвать метод закрытия с тем же intent
.
import { closePopup } from "react-use-popup";
import { intent } from "./ExampleAction";
...
<Button onClick={() => closePopup(intent)}>Close popup</Button>
Вы также можете установить обработчики, которые будут вызваны при открытии / закрытии попапа
Handbook
Передача параметров в компонент в попапе
Просто передайте их как пропсы. Или используйте контексты, хуки и т.д. из вашей доменной области
Вы можете использовать
useEffect
на пропсы как обычно
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>
);
};
В динамических попапах удобно использовать useEffect
на маунт
const Article: FC<{ id: UUID }> = props => {
const { id } = props;
useEffect(() => {
fetchArticle(id);
}, []);
return <div>Article content</div>;
};
Передача параметров при открытии
Это актуально для статических попапов. В динамических попапах вероятно проще использовать
useEffect
на маунт (см. выше)
Вы можете передать объект с параметрами в метод открытия попапа
import { openPopup } from "react-use-popup";
import { intent } from "./ExampleAction";
...
const openHandler = useCallback(
() => openPopup(intent, { userId }),
[userId]
);
...
<Button onClick={openHandler}>Open popup</Button>
Эти параметры будут переданы в перехватчик открытия и вы сможете обработать их
const ExampleAction: FC = () => {
const visible = usePopup(intent, {
open: ({ userId }) => sendAnalytics("popup opened", intent, userId)
});
return (
<Popup visible={visible}>
...
</Popup>
);
};
В этом кейсе не рекомендуется менять пропсы компонента в попапе.
Лучше вызвать метод из компонента напрямую (см. ниже)
Загрузка данных при открытии попапа
Идея в том чтобы логика компонента внутри попапа не знала о том, что он находится в попапе.
Однако, если мы не можем использовать useEffect
на маунт (например в статических попапах), то можно передать управление наружу (лучше всего с помощью 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>
);
};
Это позволяет избежать лишних ререндеров а также позволяет экспортировать дополнительные методы
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>;
};
Отправка формы из попапа перед закрытием
- Форма в попапе сама управляет логикой отправки данных сервер, а чтобы попап закрылся после успешной отправки, нужно вызвать метод закрытия попапа. Для этого передадим его в форму
- А из формы экспортируем контроллер отправки и повесим его на кнопку в попапе
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>
);
};
Внутри компонента, который будет в попапе мы описываем логику отправки, так как находимся непосредственно в бизнес-логике приложения.
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>;
};
Тут форма сама управляет логикой отправки себя на сервер
- если ошибка - показываем ошибку
- если успех - тогда после отправки закрываем попап
Открытие второго попапа для подтверждения
Просто создайте еще один экшн именно для подтверждения (мб можно даже универсальный сделать)
теперь просто открываем новый экшн поверх старого и передаем ему обработчик confirm формы
Работа с роутером - реакция на изменение url
const { pathname } = useLocation();
useEffect(() => {
if (!pathname.endsWith("/popup")) return;
openPopup(intent);
}, [pathname]);
Изменение урла при открытии попапа не имеет смысла - лучше просто изменить урл + использовать код выше → поведение будет тоже самое
Бонус - мультиинстансинг
это когда попап один и тот же, при этом открыто несколько окон одновременно, а содержимое разное
Тут можно разрулить на уровне Action
Единственное, так как история кастомная, нужно будет не использовать хук usePopup
, а самостоятельно сделать обработчики - они должны создавать инстанс попапа и добавлять в список, который будет рендериться в этом Action
issue: Поддержка мультиинстансинга
Installation
$ npm install react-use-popup
API / Types
openPopup
openPopup<OpenParams>(intent: string, detail?: OpenParams): void
открывает попап с указанным intent
и передает параметры в обработчик открытия
closePopup
closePopup<CloseParams>(intent: string, detail?: CloseParams): void
закрывает попап с указанным intent
и передает параметры в обработчик закрытия
usePopup react-hook
usePopup(intent: string, hooks?: UsePopupHooks<OpenParams, CloseParams>): boolean
возвращает состояние открытия попапа с указанным intent
и позволяет установить обработчики открытия и закрытия
type UsePopupHooks<OpenParams, CloseParams> = {
open?: (detail: OpenParams) => void;
close?: (detail: CloseParams) => void;
}