DEV Community 👩‍💻👨‍💻

devterminal
devterminal

Posted on

Using modal dialogs in React via useModal hook. As simple as possible.

The easiest way to interact with the user through modal dialogs is to use browser APIs like window.alert, window.confirm, window.prompt. There is only one problem. You cannot customize their appearance.

<body>
  <div>
    <button class="trigger">I want to drink something!</button>
    <div class="result"></div>
  </div>
  <script>
    const resultElement = document.querySelector(".result");
    const setDrink = (drink) => {
      if (drink) {
        resultElement.innerText = `Enjoy your drink, ${drink}`;
      } else {
        resultElement.innerText = ``;
      }
    };

    document.querySelector(".trigger").addEventListener("click", () => {
      setDrink(null);

      const coffee = window.confirm("Do you like coffee?");
      if (coffee) return setDrink("Coffee");

      const tea = window.confirm("Do you like tea?");
      if (tea) return setDrink("Tea");

      setDrink("Tequila");
    });
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

Let's imagine we do it in (almost) the same way, but using React:

const OrderDrink = () => {
  const [drink, setDrink] = useState();
  const modal = useModal();

  return (
    <div>
      <button
        onClick={async () => {
          setDrink(null);

          const coffee = await modal.open("Do you like coffee?");
          if (coffee) return setDrink("Coffee");

          const tea = await modal.open("Do you like tea?");
          if (tea) return setDrink("Tea");

          return setDrink("Tequila");
        }}
      >
        I want to drink something!
      </button>
      <div>{drink && `${drink} is your drink!`}</div>
      <ConfirmModal {...modal} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Looks good, doesn't it. The magic happens inside the useModal hook. Just create Promise inside the open function. And resolve it inside the close function with the result (payload) of the user interaction.

const useModal = (initial = {}) => {
  const state = useMemo(() => ({ resolve: null, data: initial.data }), []);
  const [opened, setOpened] = useState(initial.opened);

  const open = useCallback((data) => {
    setOpened(true);
    state.data = data;
    return new Promise((resolve) => { state.resolve = resolve; });
  }, []);

  const close = useCallback((payload) => {
    setOpened(false);
    state.resolve(payload);
  }, []);

  return { opened, data: state.data, close, open };
};
Enter fullscreen mode Exit fullscreen mode

And one more step - the modal dialog component:

const ConfirmModal = (props) => {
  if (!props.opened) {
    return null;
  }

  return (
    <aside>
      <div>
        {props.data}
        <br />
        <button onClick={() => props.close(true)}>yes</button>
        <button onClick={() => props.close(false)}>no</button>
      </div>
    </aside>
  );
};
Enter fullscreen mode Exit fullscreen mode

You can create a set of some reusable dialogs: AlertModal, ConfirmModal, PromptModal. And depending on specific use cases, it could be a dialog with domain specific logic like EditProductModal.

Source: https://gist.github.com/dsvgit/2f0a34201d1b075fdd42d2a79d5c5fc8
Sandbox: https://codesandbox.io/s/use-modal-hook-5nnr1v

Latest comments (0)

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

⭐️🎀 JavaScript Visualized: Promises & Async/Await

async await