How to create reusable custom modal component in React?
Asked Answered
M

3

5

I have a problem with the concept of modals in React. When using server side rendered templates with jQuery I was used to have one empty global modal template always available (included in base template that was always extended). Then when making AJAX call I just populated modal..something like this:

$('.modal-global-content').html(content);
$('.modal-global').show();

So how do I make this concept in React?

Mcinerney answered 21/6, 2020 at 18:41 Comment(2)
You don't do something like that in React, is the short answer. You want to conditionally render a modal component instead. To share functionality between modals, create a shared modal component, or use an off-the-shelf one: github.com/reactjs/react-modalLaundrywoman
@Laundrywoman Well I think I am trying to create a shared modal, but I am struggling to understand how to fill this modal (with another component) in ajax call. axios.get('/api/product/' + state.target.id + '/locales' ).then(res => { /* FILL THE MODAL WITH ANOTHER COMPONENT AND ITS DATA HERE */ });Mcinerney
C
8

There are a few ways of doing this. The first involves passing in the modal state from a parent component. Here's how to do this - first with the parent App.js component:

// App.js

import React from "react";

import Modal from "./Modal";

const App = () => {
  const [showModal, updateShowModal] = React.useState(false);

  const toggleModal = () => updateShowModal(state => !state);

  return (
    <div>
      <h1>Not a modal</h1>
      <button onClick={toggleModal}>Show Modal</button>
      <Modal canShow={showModal} updateModalState={toggleModal} />
    </div>
  );
}

export default App;

And here's the Modal.js child component that will render the modal:

// Modal.js

import React from "react";

const modalStyles = {
  position: "fixed",
  top: 0,
  left: 0,
  width: "100vw",
  height: "100vh",
  background: "blue"
};

const Modal = ({ canShow, updateModalState }) => {
  if (canShow) {
    return (
      <div style={modalStyles}>
        <h1>I'm a Modal!</h1>
        <button onClick={updateModalState}>Hide Me</button>
      </div>
    );
  }

  return null;
};

export default Modal;

This way is perfectly fine, but it can get a bit repetitive if you're reusing the modal in many places throughout your app. So instead, I would recommend using the context API.

Define a context object for your modal state, create a provider near the top of your application, then whenever you have a child component that needs to render the modal, you can render a consumer of the modal context. This way you can easily nest your modal deeper in your component tree without having to pass callbacks all the way down. Here's how to do this - first by creating a context.js file:

// context.js

import React from "react";

export const ModalContext = React.createContext();

Now the updated App.js file:

// App.js

import React from "react";

import { ModalContext } from "./context";
import Modal from "./Modal";

const App = () => {
  const [showModal, updateShowModal] = React.useState(false);

  const toggleModal = () => updateShowModal(state => !state);

  return (
    <ModalContext.Provider value={{ showModal, toggleModal }}>
      <div>
        <h1>Not a modal</h1>
        <button onClick={toggleModal}>Show Modal</button>
        <Modal canShow={showModal} updateModalState={toggleModal} />
      </div>
    </ModalContext.Provider>
  );
}

export default App;

And lastly the updated Modal.js file:

// Modal.js

import React from "react";

import { ModalContext } from "./context";

const modalStyles = {
  position: "fixed",
  top: 0,
  left: 0,
  width: "100vw",
  height: "100vh",
  background: "blue"
};

const Modal = () => {
  return (
    <ModalContext.Consumer>
      {context => {
        if (context.showModal) {
          return (
            <div style={modalStyles}>
              <h1>I'm a Modal!</h1>
              <button onClick={context.toggleModal}>Hide Me</button>
            </div>
          );
        }

        return null;
      }}
    </ModalContext.Consumer>
  );
};

export default Modal;

Here's a Codesandbox link with a working version using context. I hope this helps!

Chigetai answered 21/6, 2020 at 19:29 Comment(3)
Thanks using context is absolutely new for me. I have one perhaps silly question, in componenet called Catalogue I filter the products and here on Edit of a rpodcut I would like to render a component ProductEdit in this modal how do I call it in axios.get method?Mcinerney
@Mcinerney I think the best approach would be to save the product's idea into context as currentProductId. Then, in your Modal component, you can use useEffect to make a GET request, passing in context.currentProductId. The GET request will return the product details. You can now save these details in the local state of the Modal component and render them however you wantChigetai
With ModalContext, what if i want to change the content of the modal and show different UI elements?Woebegone
S
2

One way you can solve this problem by using css and JSX.

this is the app and i can have anything like a button a link anything Lets assume we have a link (react-router-dom) which redirects us to a DeletePage

The Delete Page renders a Modal You will provide the title and the actions of the Modal as props

const App = () => {
  return(
    <Link to="/something/someid">SomeAction</Link>
  )
}

const DeletePage = () => {
  return(
    <Modal
      title="Are you sure you want to delete this"
      dismiss={() => history.replace("/")}
      action={() => console.log("deleted") }
      />
  )
}

Modal

const Modal = (props) => {
  return(
      <div>
        <div className="background" onClick={props.dismiss}/>
        <h1>{props.title}</h1>
        <button onClick={props.dismiss}>Cancel</button>
        <button onClick={props.action}>Delete</button>
      </div>
    )
}
  • set the z-index of the modal a high number
  • position: fixed of the modal component
  • when the user will click on the background the model will go away ( many ways to implement that like with modal state, redirect, etc i have taken the redirect as one of the ways )
  • cancel button also has the same onClick function which is to dismiss
  • Delete button has the action function passed through props

this method has a flaw because of css because if your parent component has a position property of relative then this will break.
The modal will remain inside the parent no matter how high the z-index is


To Save us here comes React-Portal


React portal creates a 'portal' in its own way
The react code you might have will render inside DOM with id of #root ( in most cases )

So to render our Modal as the top most layer we create another
DOM element eg <div id="modal"></div> in the public index.html file

The Modal react component code will slightly change


const Modal = (props) => {
  return ReactDOM.createPortal(
      <div>
        <div className="background" onClick={props.dismiss}/>
        <h1>{props.title}</h1>
        <button onClick={props.dismiss}>Cancel</button>
        <button onClick={props.action}>Delete</button>
      </div>
    ),document.querySelector("#modal")
}

rest is all the same

Sandarac answered 21/6, 2020 at 19:26 Comment(4)
Thanks, but if I want to show another component in the modal can I pass it to Modal component as its prop?Mcinerney
@Mcinerney yes absolutely return <div>{props.anotherComponent}</div>Sandarac
but if I want to pass this component into modal props in ajax call?Mcinerney
@Mcinerney make the call in componentDidMount and based on the response render the Modal and pass the component as propsSandarac
S
1

Using React-Portal and Modal Generator

I have been toiling my days finding a good, standard way of doing modals in react. Some have suggested using local state modals, some using Modal Context providers and using a function to render a modal window, or using prebuilt ui libraries like ChakraUI that provides it's own Modal component. But using these can be a bit tricky since they tend to overcomplicate a relatively easy concept in web ui.

After searching for a bit, I have made peace with doing it the portal way, since it seems to be the most obvious way to do so. So the idea is, create a reusable modal component that takes children as props and using a local setState conditionally render each modal. That way, every modal related to a page or component is only present in that respective component.

Bonus:

For creating similar modals that uses the same design, you can use a jsx generator function that takes few colors and other properties as its arguments.

Working code:

// Generate modals for different types
// All use the same design
// IMPORTANT: Tailwind cannot deduce partial class names sent as arguments, and
// removes them from final bundle, safe to use inline styling
const _generateModal = (
  initialTitle: string,
  image: string,
  buttonColor: string,
  bgColor: string = "white",
  textColor: string = "rgb(55 65 81)",
  buttonText: string = "Continue"
) => {
  return ({ title = initialTitle, text, isOpen, onClose }: Props) => {
    if (!isOpen) return null;
    return ReactDom.createPortal(
      <div className="fixed inset-0 bg-black bg-opacity-80">
        <div className="flex h-full flex-col items-center justify-center">
          <div
            className="relative flex h-1/2 w-1/2 flex-col items-center justify-evenly rounded-xl lg:w-1/4"
            style={{ color: textColor, backgroundColor: bgColor }}
          >
            <RxCross2
              className="absolute top-0 right-0 mr-5 mt-5 cursor-pointer text-2xl"
              onClick={() => onClose()}
            />
            <h1 className="text-center text-3xl font-thin">{title}</h1>
            <h3 className="text-center text-xl font-light tracking-wider opacity-80">
              {text}
            </h3>
            <img
              src={image}
              alt="modal image"
              className="hidden w-1/6 lg:block lg:w-1/4"
            />
            <button
              onClick={() => onClose()}
              className="rounded-full px-16 py-2 text-xl text-white"
              style={{ backgroundColor: buttonColor }}
            >
              {buttonText}
            </button>
          </div>
        </div>
      </div>,
      document.getElementById("modal-root") as HTMLElement
    );
  };
};

export const SuccessModal = _generateModal(
  "Success!",
  checkimg,
  "rgb(21 128 61)" // green-700
);
export const InfoModal = _generateModal(
  "Hey there!",
  infoimg,
  "rgb(59 130 246)" // blue-500
);
export const ErrorModal = _generateModal(
  "Face-plant!",
  errorimg,
  "rgb(190 18 60)", // rose-700
  "rgb(225 29 72)", // rose-600
  "rgb(229 231 235)", // gray-200
  "Try Again"
);

Saccharin answered 7/2, 2023 at 16:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.