DEV Community

sheep
sheep

Posted on

Stop Repeating Modal Logic! Build a Reusable useModal Hook

Modal is a commonly used component in frontend projects. Many UI libraries provide built-in modal components, making it easy to implement semi-transparent background and dialogs with "Cancel" and "Ok" buttons.

While developing with React, I like using modals for most user interactions — whether for editing data or displaying alerts. As a result, my project became cluttered with modal components everywhere. After repeatedly writing the same logic for controlling the modals, I decided to create a custom hook to simplify the process.

Below, I’ll walk you through my implementation. Let's Go!

The Goal

I named this hook useModal. What I aimed to achieve with useModal was to extract and centralize three key behaviors:

  • opening the modal
  • closing the modal
  • passing parameters to the modal

Implementation of useModal

To achieve the goal, the essential modal behaviors will be encapsulated in the useModal hook. Let's take a look at the source code:

import React, { useState, useCallback } from 'react';

function useModal() {
  const [visible, setVisible] = useState(false);
  const [params, setParams] = useState();

  const open = useCallback((params) => {
    setParams(params);
    setVisible(true);
  }, []);

  const close = useCallback(() => {
    setVisible(false);
    setParams(undefined);
  }, []);

  const props = {
    visible,
    params,
    onClose: close,
  };

  return [props, open, close];
}
Enter fullscreen mode Exit fullscreen mode

Modals manage their own UI implementation independently. The useModal hook intentionally stays decoupled from content rendering, concentrating solely on control flow through three key returns:

  1. props - An object containing the modal's visibility state and parameters
  2. open - A function that accepts parameters and opens the modal
  3. close - A function that closes the modal and clears any parameters

Usage Of useModal

Now let's see how to make use of the useModal hook.

Let's consider a scenario where we have a page(Page) displaying a list of bookmarks(BookMarkItem). Each bookmark in the list has an edit button. When clicking this edit button, it opens a bookmark editing modal(BookMarkEditModal) and passes the corresponding bookmark data into the modal.

First we have a modal's implementation of BookMarkEditModal:

function BookMarkEditModal(props) {
  const { visible, params, onClose } = props;
  return visible ? (
    /* content of modal */
  ) : null;
}
Enter fullscreen mode Exit fullscreen mode

The BookMarkEditModal's visibility is entirely controlled by the visible prop - it only renders when visible is set to true, otherwise it returns nothing.

All necessary data flows into the modal through props: the params provide external data to the modal, while the onClose callback enables the modal to close itself, typically triggered by an internal close button. This pattern elegantly handles the common requirement where modals need to manage their own dismissal while remaining controlled by parent components.

Then let's see how BookMarkEditModal is controlled by useModal hook:

Page:

import React, { useCallback, useState } from 'react';
import BookMarkItem from './BookMarkItem'; // list item
import BookMarkEditModal from './BookMarkEditModal'; // the modal
import useModal from './useModal';

function Page() {
  const [BookMarkEditModalProps, openBookMarkModal, closeBookMarkModal] = useModal();

  const handleItemEdit = useCallback((item) => {
    openBookMarkModal(item)
  }, [openBookMarkModal]);

  return (
    <div id="page">
      {
        list.map(item => (
          <BookMarkItem onEditClick={() => handleItemEdit(item)} />
        ))
      }
        <BookMarkEditModal {...BookMarkEditModalProps} />
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

The BookMarkEditModal is rendered as part of the Page's output. From the Page, we gain access to all the necessary control mechanisms through the useModal hook - the BookMarkEditModal receives its visibility state, parameters, and close handler via props. This solution enables the parent Page component to:

  • Manage the modal's visibility by calling openBookMarkModal or closeBookMarkModal
  • Pass data to the modal when triggering the openBookMarkModal function
  • Maintain clean separation of concerns while keeping the modal's behavior predictable

The BookMarkEditModal remains completely unaware of its controlling parent, simply responding to the props it receives, which makes the modal component more reusable and easier to maintain.

Finally

With the useModal hook in place, I no longer need to repeatedly write the same open and close functions for the seven or eight modals that need to be managed across the project. The management of modal control fields (like visible) and parameters no longer gets tangled up with external components either.

The same pattern applies to any reusable logic in your application - abstract it into a custom hook, following the same approach as our useModal implementation.

Hope this little trick helps you too!
Thanks for reading!

Top comments (0)