DevGang
Авторизоваться

Псевдоимперативный подход к диалогам подтверждения в 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.

Но сохранение простоты требует сложных усилий.

Источник:

#JavaScript #React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу