DEV Community

Cover image for A pseudo imperative approach for react confirmation dialogs
BrainRepo
BrainRepo

Posted on

A pseudo imperative approach for react confirmation dialogs

Hello, this is the first technical article I am writing since we started developing fouriviere.io; for more info about the Fourier project, please visit fourviere.io.

The problem I want to discuss regards the confirmation modal; we have a few of them in our most complex flows (e.g., feed sync, feed/episode deletion).

Having a confirmation modal is often a good practice for managing un-revertable or destructive actions, and we adopted it in our critical paths for protecting the user from accidental actions.

Our frontend is built with React, and one of React's peculiarities is its very declarative approach, an approach that contrasts with the imperative approach of the confirmation modal. Considering this, our initial implementation bypassed the obstacle by effectively circumventing it; in fact, we used the tauri dialog function, which mimics the web api confirm method confirm method in a certain way.


//...do something
const confirmed = await confirm('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });

if(!confirmed){
    //...exit
} 
//...continue the action

Enter fullscreen mode Exit fullscreen mode

This is cool because it can be used in complex workflows without fighting with components and complex states; in fact, we don't need to track whether the modal is shown or the confirmation button is pressed.

However, there is a downside: the design of this confirmation modal comes from the operating system and does not fit our design styles at all.

How we solved the problem

First of all, we designed a confirmation modal, for laziness we based our component on the tailwindui dialog .

Here is an oversimplified version. If you want to see the implementation with the tailwind classes, please look at our 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>
    );
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to display this Alert modal in a portal in the most imperative way possible. To do that, we created a hook that exposes an askForConfirmation method that does all the dirty work under the hood.

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;
Enter fullscreen mode Exit fullscreen mode

This hook will return an askForConfirmation method for being called by the component logic, this method takes a Options object for defining the modal title, message and icon.

Now we need to track when modal is displayed and eventually the title, message, icon, the okAction and the cancelAction, we define a state for the component, the state can be false or object of type ModalState, if false the modal is hidden.


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;
Enter fullscreen mode Exit fullscreen mode

Now the askForConfirmation method should set the modal state, let's implement. But we want that does it following an async approach using promises, like that we can call in this way


//inside the component//

const {askForConfirmation} = useConfirmationModal()
//...previous logic
if (!await askForConfirmation()) {
    return
}
continue


Enter fullscreen mode Exit fullscreen mode

This means that askForConfirmation should return a promise that is resolved (with true or false) when the ok button is pressed or when the cancel button is pressed; before resolving the promise, the modal is hidden.


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;
Enter fullscreen mode Exit fullscreen mode

Now stays to implement the display part. This is a hook, and it does not render directly jsx; then we need to find a "sabotage" for managing the render phase. What if the hook returns a function component for rendering it?

Let's try.


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;
Enter fullscreen mode Exit fullscreen mode

Now, our hook returns aside the askForConfirmation, a function component renderConfirmationModal that displays the modal in the portal (in our case, inside the <div id="modal"> in the HTML page).

Now, let's try to use it in a simple component

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

Enter fullscreen mode Exit fullscreen mode

Conclusions

After this journey, we have a hook that helps us to have a confirmation modal with a simple api. It is essential to keep simple and reusable parts of the UI; this helps to keep the code readable, and we know how it can become messy our react components.

But keeping things simple needs complex effort.

Top comments (0)