Модальные окна - важная часть UI современных веб-приложений. Управление ими в React может вызвать трудности, в частности, когда нужно избежать одновременного появления нескольких окон. Для этого и существует хук useModalControl, который облегчает эту задачу.

Проблема

Разработчики регулярно сталкиваются с задачей контроля за состояниями множества модальных окон. Отсутствие централизованного управления может привести к путанице в процессах открытия и закрытия окон, что, в свою очередь, увеличивает риск возникновения ошибок и ухудшает общий пользовательский опыт.

Решение

Хук useModalControl предоставляет практичное решение для управления модальными окнами. Этот инструмент дает разработчикам возможность контролировать открытие и закрытие окон через простой и интуитивно понятный API. С useModalControl вы можете без труда предотвратить одновременное открытие нескольких окон, что способствует поддержанию порядка и чистоты в коде.

Live Demo

Результат

Как это работает

Хук useModalControl использует единый флаг в глобальном хранилище для управления всеми модальными окнами приложения. Это исключает риск случайного открытия неправильного окна или одновременного появления нескольких окон. Благодаря строгой типизации, вы всегда будете в курсе, какие варианты окон доступны для использования. Кроме того, useModalControl позволяет передавать в модальное окно дополнительные данные, которые будут актуальны исключительно для текущего открытого окна и сохранятся до его закрытия. Типизация передаваемых данных обеспечивает дополнительную уверенность и удобство в использовании хука.

Пошаговая инструкция: Реализация на стеке React, Redux Toolkit, TypeScript

Шаг 1.
Создаем файл с уникальными идентификаторами для наших модальных окон. Этот файл будет служить централизованным хранилищем имен окон, и его необходимо обновлять при добавлении новых модальных окон в приложение.

// modalNames.ts

export const ModalNames = {
  reset: "reset" as const,
  loading: "loading" as const,
  success: "success" as const,
  error: "error" as const,
  warning: "warning" as const,
  product: "product" as const,
}

Ключевое слово as const в конце необходимо для того, чтобы хук useModalControl возвращал точные названия доступных модальных окон. Это обеспечивает строгую типизацию и помогает избежать ошибок при работе с модальными окнами. Далее вы увидите, как это применяется на практике.

Благодаря возможности хука useModalControl передавать дополнительные данные в конкретное модальное окно, мы можем улучшить типизацию этих данных. Для каждого модального окна мы точно определим тип данных, который оно может принять. Если модальное окно не предполагает принятия данных, используем тип void. Этот подход облегчает понимание, какие данные доступны для каждого конкретного модального окна, и исключает риск передачи некорректных данных.

// modalNames.ts

type ModalNameChecker<T extends { [K in keyof typeof ModalNames]: unknown }> = T

export type SpecificModalDataType = ModalNameChecker<{
  reset: void
  loading: void
  success: string
  error: string
  warning: void
  product: ProductType
}>

Весь файл целиком

// modalNames.ts

import { ProductType } from "../../types"

export const ModalNames = {
  reset: "reset" as const,
  loading: "loading" as const,
  success: "success" as const,
  error: "error" as const,
  warning: "warning" as const,
  product: "product" as const,
}

type ModalNameChecker<T extends { [K in keyof typeof ModalNames]: unknown }> = T

export type SpecificModalDataType = ModalNameChecker<{
  reset: void
  loading: void
  success: string
  error: string
  warning: void
  product: ProductType
}>

Шаг 2.
Создаем срез modalSlice в глобальном хранилище, который будет отвечать за хранение уникального имени активного модального окна и, при необходимости, дополнительных данных, связанных с этим окном.

// modalSlice.ts

import type { PayloadAction } from "@reduxjs/toolkit"
import { createSlice } from "@reduxjs/toolkit"
import {
  ModalNames,
  SpecificModalDataType,
} from "../hooks/useModalControl/modalNames"

export type ModalSliceType<
  T extends keyof typeof ModalNames = keyof typeof ModalNames,
> = T extends infer K
  ? K extends T
    ? {
        modalData: {
          name: K
          value: SpecificModalDataType[K]
        }
      }
    : never
  : never

const initialState: ModalSliceType = {
  modalData: { name: ModalNames.reset, value: undefined },
}

export const modalSlice = createSlice({
  name: "modal",
  initialState,
  reducers: {
    setModalData: (state: ModalSliceType, action: PayloadAction<any>) => {
      state.modalData = action.payload
    },
  },
  selectors: {
    selectModalData: state => state.modalData,
  },
})

// Action creators
export const { setModalData } = modalSlice.actions

// Selectors
export const { selectModalData } = modalSlice.selectors

Шаг 3. хук useModalControl

// useModalControl.ts

import { useDispatch, useSelector } from "react-redux"
import { ModalNames, SpecificModalDataType } from "./modalNames"
import { capitalizeFirstLetter } from "./utils/capitalizeFirstLetter"
import {
  ModalSliceType,
  selectModalData,
  setModalData,
} from "../../store/modalSlice"

type ModalNameKeys = keyof typeof ModalNames
type ModalKeysType = {
  [K in ModalNameKeys as `is${Capitalize<K>}Modal`]: boolean
}
type ModalDataType = {
  [K in ModalNameKeys as `${K}ModalData`]?: SpecificModalDataType[K]
}

type ModalControlType = ModalKeysType & {
  options: {
    modalData: ModalDataType
    openModal: <K extends ModalNameKeys>(
      key: K,
      data?: SpecificModalDataType[K],
    ) => void
    closeModal: () => void
  }
}

export const useModalControl = (): ModalControlType => {
  const currentModalData = useSelector(selectModalData)

  const dispatch = useDispatch()

  const matches = {} as ModalKeysType
  let ModalKey: keyof typeof ModalNames
  for (ModalKey in ModalNames) {
    const key =
      `is${capitalizeFirstLetter(ModalKey)}Modal` as keyof ModalKeysType
    matches[key] = ModalNames[ModalKey] === currentModalData.name
  }

  return {
    ...matches,
    options: {
      modalData: {
        [`${currentModalData.name}ModalData`]: currentModalData.value,
      },
      openModal: (key, data) => {
        dispatch(
          setModalData({
            name: key,
            value: data,
          } as ModalSliceType["modalData"]),
        )
      },
      closeModal: () => {
        dispatch(
          setModalData({
            name: ModalNames.reset,
            value: undefined,
          }),
        )
      },
    },
  }
}

Вспомогательная функция для перевода первой буквы в верхний регистр capitalizeFirstLetter

// capitalizeFirstLetter.ts

export const capitalizeFirstLetter = (str?: string | undefined) => {
  if (!str) return ""

  return str.replace(/^\w/, c => c.toUpperCase())
}

useModalControl возвращает объект, который не только содержит идентификаторы для каждого окна, а также объект options, который содержит: openModal — функция для активации открытия модального окна, и closeModal — функция для его закрытия, объект modalData предоставляет детализированную информацию для каждого окна.

Возвращаемые значения
options
modalData

После того как вы выполните указанные шаги, хук useModalControl будет готов к использованию в вашем приложении.

Примеры использования

1. Открытие и закрытие модальных окон

Добавляем обработчики событий onClick
С помощью хука useModalControl для каждого модального окна задается свой уникальный флаг
Интегрируем компоненты с модальными окнами и кнопками в компонент App
Результат открывания окон по нажатию на кнопки
  1. Пример с передачей данных в модальное окно

    На примере таблицы с продуктами реализуем открытие модального окна для каждой позиции и передачу в него данных.

Добавляем идентификатор для модального окна, затем определяем тип данных, которые будем передавать.
Навешиваем обработчик на клик по каждому ряду таблицы, передавая в функцию открытия окна идентификатор и данные, соответствующие ожидаемому типу.
Принимаем переданные данные в модальном окне для каждой продуктовой позиции.
Размещаем модальное окно с информацией о продукте в компоненте App
Результат!!!

Резюме

Хук useModalControl делает работу с модальными окнами простой и удобной.
Благодарю за внимание к статье! Ваша обратная связь будет очень ценной!

Репозиторий с приложением можно найти на GitHub
Демонстрация приложения здесь
Мой профиль Linkedin