Lukasz Drazewski

personal blog

Custom React hooks: useModal

Life has become much simpler since hooks were introduced in React (v16.8). For many developers, class components are forgotten, and managing the state or side effects of (functional) components has become much easier and more enjoyable. In addition to the Standard Hooks such as useState and useEffect, we have another great option - to create our own Custom Hooks.


This is how I wanted to start a series of articles in which I will describe a few of these custom hooks that I have created and regularly use on various projects. They are quite universal, so I hope they will be useful to you as well. I start the series with a very simple and probably used by many of you Hook to manage the display of modals:

useModal

Popular UI libraries used in React such as Ant Design, React Bootstrap or Material UI have Modal or Dialog component. A modal is nothing more than a pop-up window requiring user interaction (or not) displaying some information. An example of such a modal can be a window in which the application asks you to confirm that you want to log out, delete something, or simply inform the user about an error.

Using the Modal Component, which is taken directly from the React-Bootrstrap documentation, is as follows:

import React, { useState } from "react";
import {
    Card, 
    Button,
    Row,
    Modal,
} from "react-bootstrap";

const TestComponent = () => {
  const [ isModal, setIsModal ] = useState();

  const handleShowModal = () => setIsModal(true);

  return (
    <>
      <Row className="justify-content-md-center">
        <Card body>
            <h2>Click here:</h2>
            <Button onClick={handleShowModal}>
              open modal
            </Button>
        </Card>
      </Row>
      <Modal show={isModal} backdrop="static" keyboard={false}>
        <Modal.Header>
        <Modal.Title>Modal title</Modal.Title>
        </Modal.Header>
        <Modal.Body>Hello world ???</Modal.Body>
        <Modal.Footer>
          <Button variant="secondary" onClick={() => setIsModal(false)}>Cancel</Button>
          <Button variant="primary" >OK</Button>
        </Modal.Footer>
      </Modal>
    </>
  )
}

export default TestComponent;

These types of components are usually used many times in various places of the application. So why not make one reusable component that will take parameters such as: title, content, button names and actions? Great, let's create a component like this and call it ModalContainer. It will accept the props necessary to display Modal, all texts and the submit button action. Let's assume that the only action after clicking the Cancel button is to close the Modal.

import { Button, Modal } from "react-bootstrap";

const ModalContainer = ({
  title, body, showCancelButton, confirmButtonText, confirmButtonAction, show, hideModal  
}) => {
  return (
    <Modal show={show} backdrop="static" keyboard={false}>
      <Modal.Header>
      <Modal.Title>{title}</Modal.Title>
      </Modal.Header>
      <Modal.Body>{body}</Modal.Body>
      <Modal.Footer>
        {showCancelButton &&
          <Button variant="secondary" onClick={hideModal}>Cancel</Button>
        }
          <Button variant="primary" onClick={confirmButtonAction}>{confirmButtonText}</Button>
      </Modal.Footer>
    </Modal>
  )
}

And that's great, now we can easily reuse this component in other components just by passing necessary props. However, when I added several such components in different places, sometimes even several times in one component, I decided to separate a special function that would be responsible for displaying Modal. This function will return two values: setModal - that is a function that allows us to set modal's content, and modalComponent - ready modal component to display. This will be our useModal hook.

export const useModal = () => {
  const [isModal, setIsModal] = useState(false);
  const [modalContent, setModalContent] = useState();
  const [modalComponent, setModalComponent] = useState();
  const [modalSize, setModalSize] = useState();

  const hideModal = () => {
    setIsModal(false);
  }

  const setModal = (content, size) => {
    if (!content) {
      setIsModal(false);
      setModalContent(null);
    } else {
      setIsModal(true);
      setModalContent(content);
      if (size) setModalSize(size);
    }
  };

  useEffect(() => {
    if (modalContent) {
      const {title, body, confirmButtonAction, confirmButtonText} = modalContent;

      setModalComponent(
        <ModalContainer
          size={modalSize}
          title={title}
          body={body}
          showCancelButton
          confirmButtonText={confirmButtonText}
          confirmButtonAction={confirmButtonAction}
          show={isModal}
          hideModal={hideModal}
        />
      );
    }
  }, [modalContent, isModal]);

  return { setModal, modalComponent };
};

Using such a hook will be very simple.

const TestComponent = () => {
  const { setModal, modalComponent }  = useModal();

  const handleShowModal = () => setModal(
    {
      title: 'Modal title',
      body: 'Modal body',
      confirmButtonAction: () => console.log('action!'),
      confirmButtonText: 'OK',
    },'sm'
  );

  return (
    <>
      <Row className="justify-content-md-center">
        <Card body>
            <h2>Click here:</h2>
            <Button onClick={handleShowModal}>
              open modal
            </Button>
        </Card>
      </Row>
      {modalComponent}
    </>
  )
}

As you may have noticed, I added one more props to pass in addition to the content. This is the size that is passed through the hook to the Modal component. The Bootstrap Modal can have three size values: 'sm' | 'lg' | 'xl' .

Proper use of this Hook requires that each time we need to render the returned modalComponent. Importantly, however, if we wanted to use several different modals in one component, we only need to use one modalComponent.

const TestComponent = () => {
  const { setModal, modalComponent }  = useModal();

  const handleShowModal = () => setModal(
    {
      title: 'Modal title',
      body: 'Modal body',
      confirmButtonAction: () => console.log('action!'),
      confirmButtonText: 'OK',
    },'sm'
  );

  const handleShowAnotherModal = () => setModal(
    {
      title: 'Another Modal title',
      body: 'Another Modal body',
      confirmButtonAction: () => {},
      confirmButtonText: 'Submit',
    },'lg'
  );

  return (
    <>
      <Row className="justify-content-md-center">
        <Card body>
            <h2>Click here:</h2>
            <Button onClick={handleShowModal}>
              open modal
            </Button>
            <Button onClick={handleShowAnotherModal}>
              open another modal
            </Button>
        </Card>
      </Row>
      {modalComponent}
    </>
  )
}

And here you can see how it works in practice: useModal on Codepen

See the Pen modal by drazewski (@drazewski) on CodePen.


I hope that the above example of this cutom hook will be useful to someone in everyday work with React. If you see other possibilities for creating such a Hook, or writing it otherwise, please write in the comment below.