Home > Blockchain >  How to implement a use-modal-hook with a alert/confirm/prompt pattern?
How to implement a use-modal-hook with a alert/confirm/prompt pattern?

Time:12-16

I've started using ReactModal to display warning, prompts and such to the user. This works well, but you have to add the component and isOpen state and callbacks everywhere you want to maybe display a modal window. Maybe I've misunderstood how to use it properly, but this pattern quickly becomes tedious.

I want to implement a sort of alert, confirm, prompt pattern where I can just say e.g:

const { myAlert, myConfirm } = useModal();

if(myConfirm('Show an alert?')) {
  myAlert('Alert!');
}

to show my modal in the desired way.

My problem lies with the functions. I want the app to wait for the user to click something in the modal before the following code is executed. Now my functions just instantly return after executing.

Here is my hook:

import { useContext } from 'react';
import { ModalContext } from '../App';
import Alert from '../components/Modals/Alert';
import Confirm from '../components/Modals/Confirm';

const useModal2 = () => {
  const { openModal, closeModal } = useContext(ModalContext);

  const alert = (message: string) => {
    openModal(
      <Alert title="Warning" message={message} closeAlert={closeModal} />
    );
  };
  const confirm = (question: string): boolean | undefined => {
    let answer;
    const closeConfirm = (ok: boolean) => {
      answer = ok;
      closeModal();
    };
    openModal(
      <Confirm
        title="Warning"
        question={question}
        closeConfirm={closeConfirm}
      />
    );
    return answer;
  };

  return { alert, confirm };
};

export default useModal2;

here is my Context and its values:

interface ModalContextType {
  openModal: (modal: ReactNode) => void;
  closeModal: () => void;
}
export const ModalContext = createContext<ModalContextType>({
  openModal: () => {},
  closeModal: () => {},
});
...
const App = () => {
...
  const [modalContent, setModalContent] = useState<ReactNode>(null);
  const [isModalOpen, { openModal, closeModal }] = useModal();
  const open = (modal: ReactNode) => {
    setModalContent(modal);
    openModal();
  };
  const close = () => {
    setModalContent(null);
    closeModal();
  };
...
return (
  <ModalContext.Provider value={{ openModal: open, closeModal: close }}>
...
        <ReactModal
          className={styles.modalContent}
          overlayClassName={styles.modalOverlay}
          isOpen={isModalOpen}
          onRequestClose={close}
        >
          {modalContent}
        </ReactModal>
    ...

If I click the following button, the onClick function just outputs undefined before I click anything in the confirm dialog:

      <button
        onClick={() => {
          const answer = confirm('Show an alert?');
          if (answer) {
            alert('The alert you requested!');
          }
          console.log(answer);
        }}
      >
        Confirm!
      </button>

how can I make it wait for my answer in the dialog before it checks the value in the if and runs the console.log? Any ideas?

Here is my code in codesandbox: https://codesandbox.io/s/blue-architecture-jtjyr

CodePudding user response:

To answer your main question:

Your confirm function will return as soon as it is called. It will not wait for closeConfirm to set the answer variable, because JS functions are synchronous by nature, their content is executed from top to bottom right away. Except for asyncronous functions or generators, which will probably not solve your problem. You could probably solve it with a state variable but that will complicate things even further.

const confirm = (question) => {
    let answer;
    const closeConfirm = (ok) => {
      answer = ok;
      closeModal();
    };
    openModal(
      <Confirm
        title="Warning"
        question={question}
        closeConfirm={closeConfirm}
      />
    );
    return answer;
  };

You could try to visualize the execution of that snippet to better understand it. JS execution visualizer

Furthermore it seems like you are overcomplicating things by a lot here. I think you don't need context here, but I don't know the whole context of your app, pun intended ;). You are overriding browser globals like alert(), which is a bad practive imo. You are mapping and nesting functions like open > openModal > alert... And your naming is not descriptive. Like what is the difference between useModal and useModal2? So in total the code is hard to read.

Here is a codesandbox to give you an idea of how you could solve it in a slightly more readable way.

CodePudding user response:

Just had to return a Promise from the functions supplied by my hook.

import { useContext } from 'react';
import { ModalContext } from '../App';
import Alert from '../components/Modals/Alert';
import Confirm from '../components/Modals/Confirm';

const useDialog = () => {
  const { openModal, closeModal } = useContext(ModalContext);

  const alertDialog = (message: string) => {
    return new Promise((resolve) => {
      const closeAlert = () => {
        resolve(true);
        closeModal();
      };

      openModal(
        <Alert title="Warning" message={message} closeAlert={closeAlert} />
      );
    });
  };
  const confirmDialog = (message: string) => {
    return new Promise<boolean>((resolve) => {
      const closeConfirm = (value: boolean) => {
        resolve(value);
        closeModal();
      };

      openModal(
        <Confirm
          title="Warning"
          message={message}
          closeConfirm={closeConfirm}
        />
      );
    });
  };

  return { alertDialog, confirmDialog };
};

export default useModal2;

then you can use it like so:

      <button
        onClick={() => {
          const answer = await confirmDialog('Show an alert?');
          if (answer) {
            await alertDialog('The alert you requested!');
          }
          console.log('Done showing dialogs for now!');
        }}
      >
        Confirm!
      </button>

Would probably be cleaner to take care of the async/await somewhere else than where you open the dialogs. But thats a problem for another time.

  • Related