DEV Community

Cover image for Your next React Modal with your own "useModal" Hook & Context API.
Alex Suarez
Alex Suarez

Posted on • Edited on

Your next React Modal with your own "useModal" Hook & Context API.

Hi there everyone, this is a quick review about how to use Modals Components in your React project combining Hooks, Context, and Portals. You need to have some experience coding with React, and be aware of React's latest updates like Hooks and Context API. Let's do it.

The Modal Component

Before writing our Modal Component, let's open our public/index.html (or the HTML where you render your JS code) and add a new tag to render out the Modal Component using a React Portal.

<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <div id="modal-root"></div>
  <div id="root"></div>
</body>
Enter fullscreen mode Exit fullscreen mode

Now let's write our Modal Component, and use the createPortal function, createPortal function expects two parameters, the first is the actual JSX and the second the DOM element where it will be rendered it.

import React from "react";
import ReactDOM from "react-dom";

const Modal = () => {
  return ReactDOM.createPortal(
    <div
      className="fixed top-0 left-0 h-screen w-full flex items-center justify-center"
      style={{ background: "rgba(0,0,0,0.8)" }}
    >
      <div className="bg-white relative p-5 shadow-lg rounded flex flex-col items-start text-lg text-gray-800">
        <button
          className="absolute top-0 right-0 -mt-12 font-bold self-end rounded-full bg-red-200 mb-3 bg-white text-red-700 w-8 h-8"
          onClick={() => {}}
        >
          &times;
        </button>
        <p>I am the Modal</p>
      </div>
    </div>,
    document.querySelector("#modal-root")
  );
};

export default Modal;

Enter fullscreen mode Exit fullscreen mode

useModal Hook

This custom Hook is going to hold our Modal Component states, but first let's remind what a Hook is according react docs:

React Hooks are functions that let us hook into the React state and lifecycle features from function components. By this, we mean that hooks allow us to easily manipulate the state of our functional component without needing to convert them into class components

In other words, Hooks allow us to create "shareable models" of states and methods to manipulate those states, by returning both we can reuse it across components, and we can avoid code duplication in our project. If we have more than one component that initializes the same state structure and methods it may be a good idea to extract those in a custom hook, and we can have the state and the methods in one place and reuse it. This is our custom useModal React Hook.

import React from "react";

export default () => {
  let [modal, setModal] = React.useState(false);
  let [modalContent, setModalContent] = React.useState("I'm the Modal Content");

  let handleModal = (content = false) => {
    setModal(!modal);
    if (content) {
      setModalContent(content);
    }
  };

  return { modal, handleModal, modalContent };
};

Enter fullscreen mode Exit fullscreen mode

Every Hook we create, as a rule, needs to start with the word "use".
Now you may think you can share actual states values across components with Hooks ... Sadly the answer is NO, every time you use a Hook in a component and you extract the state from the Hooks, this creates a "local state" only visible within that component, if you want to pass that actual state to a children component this has to be done via props or in this case using React Context

React Context

We are going to use our newly created React Hook in our ModalContext...

import React from "react";
import useModal from "./useModal";
import Modal from "./modal";

let ModalContext;
let { Provider } = (ModalContext = React.createContext());

let ModalProvider = ({ children }) => {
  let { modal, handleModal, modalContent } = useModal();
  return (
    <Provider value={{ modal, handleModal, modalContent }}>
      <Modal />
      {children}
    </Provider>
  );
};

export { ModalContext, ModalProvider };
Enter fullscreen mode Exit fullscreen mode

Now let's do a simple modification in our modal component to start using our context info there as props.

import React from "react";
import ReactDOM from "react-dom";
import { ModalContext } from "./modalContext";

const Modal = () => {
  let { modalContent, handleModal, modal } = React.useContext(ModalContext);
  if (modal) {
    return ReactDOM.createPortal(
      <div
        className="fixed top-0 left-0 h-screen w-full flex items-center justify-center"
        style={{ background: "rgba(0,0,0,0.8)" }}
      >
        <div className="bg-white relative p-5 shadow-lg rounded flex flex-col items-start text-lg text-gray-800">
          <button
            className="absolute top-0 right-0 -mt-12 font-bold self-end rounded-full bg-red-200 mb-3 bg-white text-red-700 w-8 h-8"
            onClick={() => handleModal()}
          >
            &times;
          </button>
          <p>{modalContent}</p>
        </div>
      </div>,
      document.querySelector("#modal-root")
    );
  } else return null;
};

export default Modal;

Enter fullscreen mode Exit fullscreen mode

Now let's move to the app.js component and let's start using our Modal Component and the Context Provider

import React from "react";
import { ModalProvider } from "./modalContext";
import Component from "./component";
import Component2 from "./component2";

export default function App() {
  return (
    <div className="App container mx-auto px-8 text-gray-700">
      <h1 className="text-3xl">Hello CodeSandbox</h1>
      <h2 className="text-xl mb-6">Start editing to see some magic happen!</h2>
      <ModalProvider>
        <Component />
        <Component2 />
      </ModalProvider>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

You will notice a couple of components there "Component and Component2" those are some dummy components that hold a button to open the Modal, the main difference between them is the message to render inside our Modal

import React from "react";
import { ModalContext } from "./modalContext";

const Component = () => {
  let { handleModal } = React.useContext(ModalContext);

  return (
    <>
      <p>
        Lorem ipsum dolor sit amet consectetur, adipisicing elit. Cumque quidem
        asperiores?
      </p>
      <button
        className="mt-6 rounded  bg-purple-700 text-purple-100 px-5 h-12"
        onClick={() => handleModal("This is component modal content")}
      >
        open this modal!
      </button>
    </>
  );
};

export default Component;
Enter fullscreen mode Exit fullscreen mode

You will end it up with something like this CodeSandbox Modal Demo

And that's it, I tried to make this as short as possible without digging into the code's specific parts, please if you have any doubts about the code or a different approach let me know at the comments.

Picture by Rodolpho Zanardo, Pexels

For Rhys Nicholls "closing the modal from within"
You can pass a component to handleModal function instead of a string then in that component you can destructure the handleModal function from the context and call that function on demand just like this...
component

function ContentComponent() {
  let { handleModal } = React.useContext(ModalContext);
  return (
    <>
      <p>Hello here !!!</p>
      <button
        className="h-8 px-3 text-white bg-red-500 text-xs rounded"
        onClick={handleModal}
      >
        Close modal
      </button>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

and then import this component and use it in the handleModal

const Component = () => {
  let { handleModal } = React.useContext(ModalContext);

  return (
    <>
      <p>
        Lorem ipsum dolor sit amet consectetur, adipisicing elit. Cumque quidem
        asperiores?
      </p>
      <button
        className="mt-6 rounded  bg-purple-700 text-purple-100 px-5 h-12"
        onClick={() => handleModal(<ContentComponent />)}
      >
        open this modal!
      </button>
    </>
  );
};

Enter fullscreen mode Exit fullscreen mode

You may see the live example here https://codesandbox.io/s/eloquent-hamilton-vgbyq?file=/src/component.js:75-508https://codesandbox.io/s/eloquent-hamilton-vgbyq?file=/src/component.js:75-508

For Joel Robles Bentham "Open the modal on page load"
Simple call it on component mount, use and effect for it like

// the string could be a component as as well
 React.useEffect(() => {
    handleModal("This is component 2 modal content on page load");
  }, []);
Enter fullscreen mode Exit fullscreen mode

live example here https://codesandbox.io/s/eloquent-hamilton-vgbyq?file=/src/component2.js:160-261

Top comments (13)

Collapse
 
rhysnicholls profile image
Rhys Nicholls • Edited

Hi Alex. Excellent tutorial which has helped me alot and I have a follow up question
I'm wanting to close the modal using a button nested inside of modalContent passed to handleModal.
How would I go about this?
I have tried using handleModal for the onClick of the button but nothing happens.
This may be a hole in my knowledge

Collapse
 
alexandprivate profile image
Alex Suarez

Done Rhys, article updated!

Collapse
 
alexandprivate profile image
Alex Suarez

Hi there Rhys sorry for the late response, let's revamp the article to show that use case, I will address that part in a section with you question

Collapse
 
james12 profile image
James

Hello,

First of all, apologies for commenting on a post from over a year ago and if you're still active here, then thank you.

I am wondering how to go about implementing the scenario where state needs to be shared between the modal and component from where it's rendered. I somehow need a way to re-render the modal if a prop changes

This is a codesandbox of a simple example: codesandbox.io/s/unruffled-newton-...

I will keep this question updated on how I get along with trying to find a clean solution

Collapse
 
hrommi profile image
Stepan Zasimenko

Hi, nice article. But in the old version of the react documentation it is not recommended to save the components in the state. What do you think about it?

Collapse
 
alexandprivate profile image
Alex Suarez

Hi there Stepan, thanks for sharing, yes as you mentioned that's old but true!! I will not do it, in this case, this is just a quick example of how to solve a use case without fully refactoring the context, but you are totally right!! states = data.

Collapse
 
hi_natew profile image
Nate Wang

Here's a similar implementation: github.com/eBay/nice-modal-react

Collapse
 
jrbentham profile image
Joel Robles Bentham

Hi, excellent job. But i get stoked when i wanted to open a modal after some other action besides the clicking, for example if i want to open a pop up modal on page ready.
it can be done?

Collapse
 
alexandprivate profile image
Alex Suarez

Done Joel article updated

Collapse
 
alexandprivate profile image
Alex Suarez

Hi there Joel sure you can, let me revam article and we can address that use case!

Collapse
 
tomliem profile image
tomliem

Hello, i'm trying to use this. But with eslint no-cycle, i can't use this pattern, because there Modal import in context, and context import in Modal.
How can i use this approach without moving Modal inside provider?

Collapse
 
developdeez profile image
developdeez

Just tried and it doesnt seem to work with a controlled textbox component. Can you add a textbox where the parent holds the state?

Collapse
 
ifrostizz profile image
Guyot François

Very good explanation, thanks !