Избавьтесь от хаоса модальных окон с useModalControl (React)
Модальные окна - важная часть UI современных веб-приложений. Управление ими в React может вызвать трудности, в частности, когда нужно избежать одновременного появления нескольких окон. Для этого и существует хук useModalControl, который облегчает эту задачу.
Проблема
Разработчики регулярно сталкиваются с задачей контроля за состояниями множества модальных окон. Отсутствие централизованного управления может привести к путанице в процессах открытия и закрытия окон, что, в свою очередь, увеличивает риск возникновения ошибок и ухудшает общий пользовательский опыт.
Решение
Хук useModalControl предоставляет практичное решение для управления модальными окнами. Этот инструмент дает разработчикам возможность контролировать открытие и закрытие окон через простой и интуитивно понятный API. С useModalControl вы можете без труда предотвратить одновременное открытие нескольких окон, что способствует поддержанию порядка и чистоты в коде.
Как это работает
Хук 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 предоставляет детализированную информацию для каждого окна.
После того как вы выполните указанные шаги, хук useModalControl будет готов к использованию в вашем приложении.
Примеры использования
1. Открытие и закрытие модальных окон
Пример с передачей данных в модальное окно
На примере таблицы с продуктами реализуем открытие модального окна для каждой позиции и передачу в него данных.
Резюме
Хук useModalControl делает работу с модальными окнами простой и удобной.
Благодарю за внимание к статье! Ваша обратная связь будет очень ценной!
Репозиторий с приложением можно найти на GitHub
Демонстрация приложения здесь
Мой профиль Linkedin