Псевдоимперативный подход к диалогам подтверждения в React
Здравствуйте, проблема, которую я хочу обсудить, касается модуля подтверждения; у нас есть несколько таких модулей в самых сложных потоках (например, синхронизация фидов, удаление фидов/эпизодов).
Наличие модуля подтверждения часто является хорошей практикой для управления необратимыми или разрушительными действиями, и мы внедрили его в наши критические потоки, чтобы защитить пользователя от случайных действий.
Наш фронтенд построен на React, а одной из особенностей React является его очень декларативный подход, который контрастирует с императивным подходом модуля подтверждения. Учитывая это, наша первоначальная реализация фактически обошла это препятствие; по сути, мы использовали функцию tauri dialog, которая определенным образом имитирует метод подтверждения web api.
//...do something
const confirmed = await confirm('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
if(!confirmed){
//...exit
}
//...continue the action
Это здорово, потому что его можно использовать в сложных рабочих процессах без борьбы с компонентами и сложными состояниями; по сути, нам не нужно отслеживать, показан ли модуль или нажата кнопка подтверждения.
Однако есть и обратная сторона: дизайн этого модуля подтверждения заимствован из операционной системы и совершенно не подходит под наши стили оформления.
Решение проблемы
Прежде всего, мы разработали модальное окно подтверждения, из-за лени мы взяли за основу диалог tailwindui.
Здесь представлена упрощенная версия. Если вы хотите увидеть реализацию с классами tailwind, пожалуйста, посмотрите на нашу ui lib.
type Props = {
ok: () => void;
cancel: () => void;
title: string;
message: string;
okButton: string;
cancelButton: string;
icon?: React.ElementType;
};
export default function Alert({ok, cancel, title, message, okButton, cancelButton, icon}: Props) {
const Icon = icon as React.ElementType;
return (
<div>
<h3>{icon} {title}</h3>
<p>{message}</p>
<div>
<button onClick={ok}>{okButton}</button>
<button onClick={cancel}>{cancelButton}</button>
</div>
</div>
);
}
Теперь нам нужно отобразить этот модуль предупреждения на портале наиболее наглядным способом. Для этого мы создали хук, который раскрывает метод askForConfirmation
, выполняющий всю грязную работу под капотом.
interface Options {
title: string;
message: string;
icon ?: ElementType;
}
const useConfirmationModal = () => {
async function askForConfirmation({title, message, icon}:Options)
{
//Here we will put our implementation
}
return {askForConfirmation}
}
export default useConfirmationModal;
Этот хук вернет метод askForConfirmation
для вызова логикой компонента, этот метод принимает объект Options
для определения заголовка, сообщения и иконки модуля.
Теперь нам нужно отследить момент отображения модуля и, в конечном счете, заголовок (title
), сообщение (message
), иконку (icon
), действие okAction
и действие cancelAction
, мы определяем состояние для компонента, состояние может быть false
или объект типа ModalState
, если false
, то модальный элемент скрыт.
interface Options {
title: string;
message: string;
icon ?: ElementType;
}
interface ModalState
title: string;
message: string;
ok: () => void;
cancel: () => void;
icon?: ElementType;
}
const useConfirmationModal = () => {
const [modal, setModal] = useState<false | ModalState>(false);
async function askForConfirmation({title, message, icon}:Options)
{
//Here we will put our implementation
}
return {askForConfirmation}
}
export default useConfirmationModal;
Теперь метод askForConfirmation
должен установить модальное состояние, давайте реализуем его. Но мы хотим, чтобы это происходило в асинхронном режиме с использованием промисов, например, чтобы мы могли вызвать его таким образом:
//inside the component//
const {askForConfirmation} = useConfirmationModal()
//...previous logic
if (!await askForConfirmation()) {
return
}
continue
Это означает, что askForConfirmation
должен возвращать промис, который разрешается (true
или false
) при нажатии кнопки ok или при нажатии кнопки cancel; перед разрешением промиса модальная панель скрывается.
interface Options {
title: string;
message: string;
icon ?: ElementType;
}
interface ModalState
title: string;
message: string;
ok: () => void;
cancel: () => void;
icon?: ElementType;
}
const useConfirmationModal = () => {
const [modal, setModal] = useState<false | ModalState>(false);
async function askForConfirmation({title, message, icon}:Options)
{
return new Promise<boolean>((resolve) => {
setModal({
title,
message,
icon,
ok: () => {
setModal(false);
resolve(true);
},
cancel: () => {
setModal(false);
resolve(false);
},
});
});
}
return {askForConfirmation}
}
export default useConfirmationModal;
Теперь остается реализовать часть отображения. Это хук, и он не рендерит напрямую jsx; тогда нам нужно найти "саботаж" для управления фазой рендеринга. Что если хук будет возвращать функцию-компонент для рендеринга?
Давайте попробуем.
interface Options {
title: string;
message: string;
icon ?: ElementType;
}
interface ModalState
title: string;
message: string;
ok: () => void;
cancel: () => void;
icon?: ElementType;
}
const useConfirmationModal = () => {
const [modal, setModal] = useState<false | ModalState>(false);
const modals = document.getElementById("modals") as HTMLElement;
async function askForConfirmation({title, message, icon}:Options)
{
return new Promise<boolean>((resolve) => {
setModal({
title,
message,
icon,
ok: () => {
setModal(false);
resolve(true);
},
cancel: () => {
setModal(false);
resolve(false);
},
});
});
}
function renderConfirmationModal() {
return (
<>
{modal && createPortal(
<Alert
icon={modal.icon ?? ExclamationTriangleIcon}
title={modal.title}
message={modal.message}
okButton="ok"
cancelButton="cancel"
ok={modal.ok}
cancel={modal.cancel}
/>,
modals,
)
}
</>
);
return {askForConfirmation, renderConfirmationModal}
}
export default useConfirmationModal;
Теперь наш хук возвращает в сторону askForConfirmation
функцию компонента renderConfirmationModal
, которая отображает модуль на портале (в нашем случае внутри <div id="modal">
на HTML-странице).
Теперь давайте попробуем использовать её в простом компоненте!
export default function SimpleComponent() {
const {askForConfirmation, renderConfirmationModal} = useConfirmationModal()
async function doSomething() {
if(!askForConfirmation({
title: "Are you sure?",
message: "This operation cannot be reverted",
})) {
return false
}
//do stuff...
}
return <>
{renderConfirmationModal()}
<button onClick={doSomething}>DO IT/button>
</>
}
Заключение
После этого путешествия у нас есть хук, который помогает нам иметь модальное окно подтверждения с простым api. Очень важно сохранять простые и многократно используемые части пользовательского интерфейса; это помогает сохранить код читаемым, а мы знаем, как могут запутаться наши компоненты react.
Но сохранение простоты требует сложных усилий.