1.0.3 • Published 6 months ago

react-use-popup v1.0.3

Weekly downloads
-
License
Unlicense
Repository
github
Last release
6 months ago

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;
}

ROADMAP