DEV Community

Cover image for Improve modal management in React with nice-modal-react
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Improve modal management in React with nice-modal-react

Written by Madars Bišs✏️

In the age of information, the use of modals can significantly improve the UX of websites and web applications. We see them everywhere, from sites like Twitter, which uses modals to create a new tweet, to complex management systems that run in the background of almost every enterprise.

The main advantage of modals is that they are independent of the active page, meaning they can be used to add, update, delete, or view the information, they are easy to open and close, they do not require changing the current URL, and the background information is often fully or partially visible.

In this tutorial, we will explore nice-modal-react, which is a useful modal utility for React created by the developer team of eBay. They have been kind enough to make it accessible for the public after testing and using the utility internally for a year.

We will also build a demo app to apply all the reviewed features in practice. It is expected that we will be able to use modals to create new data, as well as edit and delete existing data:

CRUD Notes

For reference, here is the source code of the final project.

Why use nice-modal-react?

The nice-modal-react package is a zero-dependency utility written in TypeScript and uses context to control the state of the modals throughout the entire app.

The main advantage of the utility is promise-based modal handling. This means instead of using props to interact with the component, you can use promises to update the state.

You can easily import the modal components throughout the app or use the specific id of the component, so you do not have to import the component at all.

Closing modals is independent of the rest of the code, so you can close the component from the component itself, no matter where in the application it is shown.

It is crucial to understand that nice-modal-react is not the modal component itself. You will need to create the actual modals yourself (or use pre-built components from UI libraries like Material UI, Ant design, or Chakra).

Initializing a React app

We will first create a React app by using Create React App. Run the following command in your terminal: npx create-react-app crud-notes.

Let the setup complete and you will see a new project folder created in your current working directory.

Next, change the directory by running cd crud-notes, then start the application by running npm start. The command should open your default browser and display the React app.

If it does not open automatically, enter http://localhost:3000 in the browser's URL bar and execute.

Back in the project, navigate to the src folder, find the files App.js, App.css, index.js, and remove the content from them because we will write everything from scratch.

Also, rename App.css to styles.css and remove the index.css file.

In the newly renamed styles.css file, include the following style rules:

css
@import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap");

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Montserrat", sans-serif;
}

body {
  padding: 20px;
}

.App {
  max-width: 500px;
  margin: 0 auto;
  text-align: center;
}
Enter fullscreen mode Exit fullscreen mode

First, we declared some reset rules to margin, padding, and border-box, so all the elements are displayed equally in all browsers. We also made sure the app uses the Montserrat font.

Then, we added some padding to the body, set the app wrapper to never exceed 500px, centered it in the viewport, and centered the text inside it.

Setting up nice-modal-react

Installing the nice-modal-react package itself is as simple as running npm install @ebay/nice-modal-react. It will add a small (~2KB after gzip) and dependency-free package to your node modules.

In order to use it throughout the app, we’ll set up a separate provider that will use React Context to control the state globally.

To do that, open the index.js root file, import the NiceModal component, and wrap it around the App component:

javascript
import ReactDOM from "react-dom";
import NiceModal from "@ebay/nice-modal-react";
import App from "./App";

const rootElement = document.getElementById("root");

ReactDOM.render(
  <NiceModal.Provider>
    <App />
  </NiceModal.Provider>,
  rootElement
);
Enter fullscreen mode Exit fullscreen mode

At this point, we have set up the project to work with nice-modal-react, so we can start building individual components for the app.

Creating components

First, we need to create the individual files for the necessary components: Modal, Button, and Note. To keep everything organized we will create a separate components folder and create a separate .js file and .css file for each component.

You can create the files manually, but I would recommend using the following command to save time:

bash
mkdir components && cd components && touch Modal.js Modal.css Button.js Button.css Note.js Note.css
Enter fullscreen mode Exit fullscreen mode

Modal

Open Modal.js and include the following code:

javascript
import { useState } from "react";
import NiceModal, { useModal } from "@ebay/nice-modal-react";
import "./Modal.css";
import Button from "./Button";

const Modal = NiceModal.create(
  ({ title, subtitle, action, bgColor, note = "" }) => {
    const [input, setInput] = useState(note);
    const modal = useModal();
    return (
      <div className="background">
        <div className="modal">
          <h1>{title}</h1>
          <p className="subtitle">{subtitle}</p>
          {action === "Save" && (
            <input
              className="input"
              type="text"
              value={input}
              onChange={(e) => {
                setInput(e.target.value);
              }}
            />
          )}
          <div className="actions">
            <Button
              name={action}
              backgroundColor={bgColor}
              onClick={() => {
                if (action === "Save") {
                  if (input) {
                    modal.resolve(input);
                    modal.remove();
                    console.log("Note saved");
                  } else {
                    console.log("Note is empty");
                  }
                } else {
                  modal.resolve();
                  modal.remove();
                  console.log("Note removed");
                }
              }}
            />
            <Button
              name="Cancel"
              backgroundColor="silver"
              onClick={() => {
                modal.remove();
              }}
            />
          </div>
        </div>
      </div>
    );
  }
);

export default Modal;
Enter fullscreen mode Exit fullscreen mode

First, we imported useState to track the state of the input for add and edit actions and the NiceModal component that will be the wrapper of our modal. We also imported the external stylesheet and the Button component for the cancel action to close the modal.

We used NiceModal.create as a modal wrapper. You can think of this as creating a basic component and wrapping it into a higher-order function. It will receive the title, subtitle, action, bgColor, and note props once we import the Modal component into App.js.

The add and edit modals will have an input field where users will be able to add the note title from scratch or edit an existing note title.

The state of the input will be stored in the state variable and passed for usage in App.js. I also added a simple validation so that users cannot add empty notes.

The add and edit modals will include the save option, while the delete modal will have a delete button instead. Every modal will have a cancel button next to the save/delete to close the modal.

Open Modal.css and include the following style rules:

css
.background {
  width: 100vw;
  height: 100vh;
  position: absolute;
  left: 0;
  top: 0;
  display: grid;
  place-items: center;
  background-color: rgba(0, 0, 0, 0.7);
}

.modal {
  padding: 20px;
  width: 300px;
  border-radius: 10px;
  text-align: center;
  background-color: white;
  word-break: break-all;
}

.subtitle {
  margin-bottom: 20px;
}

.input {
  width: 100%;
  height: 25px;
  border: 1px solid silver;
  border-radius: 5px;
  padding: 0px 10px;
}

.actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-top: 20px;
}
Enter fullscreen mode Exit fullscreen mode

We set the modal background to fill all the viewport, use a black background-color with a 0.7 opacity, and center the children element, which will be the modal wrapper.

For the actual modal, we set padding, specific width, border-radius, centered the text, set the background-color to be white, as well as added a word-break to split words exceeding the wrapper width.

We set a margin below the subtitle to separate it from the input and action areas.

The input will use the entire available width, have a specific height, a border with rounded corners, and some padding on the left and right sides.

The actions area will hold a couple of Button components for the edit and delete functionality and is set to divide the available width into two columns, some gap between, and margin on top.

Button

Open Button.js and include the following code:

javascript
import "./Button.css";

const Button = ({ name, backgroundColor, onClick }) => {
  return (
    <button className="button" onClick={onClick} style={{ backgroundColor }}>
      {name}
    </button>
  );
};

export default Button;
Enter fullscreen mode Exit fullscreen mode

First, we imported the stylesheet to style the component. Then, we created a simple button component that will receive the name, backgroundColor, and onClick props once imported and used in App.js.

Open the Button.css file and include the following style rules:

css
.button {
  border: none;
  padding: 5px 10px;
  cursor: pointer;
  border-radius: 5px;
  width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

We removed the default button border, added some padding, set the cursor to be a pointer, added some border-radius for smooth corners, and set the button to fill the available width.

Note

Open the Note.js file and include the following:

javascript
import "./Note.css";
import Button from "./Button";

const Note = ({ title, onClickEdit, onClickDelete }) => {
  return (
    <div className="note">
      <p>{title}</p>
      <Button name="Edit" backgroundColor="gold" onClick={onClickEdit} />
      <Button name="Delete" backgroundColor="tomato" onClick={onClickDelete} />
    </div>
  );
};

export default Note;
Enter fullscreen mode Exit fullscreen mode

We imported the stylesheet to style the component, as well as the external Button component, so we can re-use it for edit and delete functionality.

The Note component includes the title of the note, as well as the onClickEdit and onClickDelete props for the Button components we will pass in when we import and use the Note component in App.js.

Open Note.css and include the following:

css
.note {
  display: grid;
  grid-template-columns: auto 70px 70px;
  gap: 20px;
  margin: 20px auto;
  text-align: left;
  word-break: break-all;
}

@media screen and (max-width: 400px) {
  .note {
    grid-template-columns: 1fr;
  }
}
Enter fullscreen mode Exit fullscreen mode

We set the note to use a three-column layout with a 20px gap between, while the edit and delete buttons would use the fixed width, and the rest of the available width would be for the note title. We also set the margin to the top, centered the text to be positioned on the left, and added a word-break so the longer words are automatically split.

We also created some media rules for responsiveness. For the screen widths 400px and smaller, the note will switch to the one-column layout, meaning that all the included elements (title, edit button, and delete button) will be shown directly below each other.

Implementing the logic

Now, let’s put everything together and create logic for our app. Open App.js and include this code:

javascript
import { useState } from "react";
import NiceModal from "@ebay/nice-modal-react";
import Modal from "../components/Modal";
import Note from "../components/Note";
import Button from "../components/Button";
import "./styles.css";

const noteList = [
  "My awesome third note",
  "My awesome second note",
  "My awesome first note"
];

const getNoteIndex = (e) =>
  Array.from(e.target.parentElement.parentNode.children).indexOf(
    e.target.parentElement
  );

export default function App() {
  const [notes, setNotes] = useState(noteList);

  const showAddModal = () => {
    NiceModal.show(Modal, {
      title: "Add a new note",
      subtitle: "Enter the title",
      action: "Save",
      bgColor: "lime green"
    }).then((note) => {
      setNotes([note, ...notes]);
    });
  };

  const showEditModal = (e) => {
    NiceModal.show(Modal, {
      title: "Edit the note",
      subtitle: "Rename the Title",
      action: "Save",
      bgColor: "gold",
      note: notes[getNoteIndex(e)]
    }).then((note) => {
      const notesArr = [...notes];
      notesArr[getNoteIndex(e)] = note;
      setNotes(notesArr);
    });
  };

  const showDeleteModal = (e) => {
    NiceModal.show(Modal, {
      title: "Confirm Delete",
      subtitle: `The "${notes[getNoteIndex(e)]}" will be permanently removed`,
      action: "Delete",
      bgColor: "tomato",
      note: notes[getNoteIndex(e)]
    }).then(() => {
      const notesArr = [...notes];
      notesArr.splice(getNoteIndex(e), 1);
      setNotes(notesArr);
    });
  };

  return (
    <div className="App">
      <h1>CRUD Notes</h1>
      <p style={{ marginBottom: "20px" }}>Using nice-modal-react</p>
      <Button
        name="Add"
        backgroundColor="lime green"
        onClick={() => {
          showAddModal();
        }}
      />
      <div>
        {notes.map((note, index) => {
          return (
            <Note
              key={index}
              note={note}
              onClickEdit={showEditModal}
              onClickDelete={showDeleteModal}
            />
          );
        })}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

First, we imported the useState hook to keep track of the notes object once we update it when using the app. We also imported the NiceModal component and every individual component we created in the previous phase.

To style the component, we’ll use an external stylesheet we created.

Then we created a noteList array that will hold the sample notes for the application. We also created the getNoteIndex function so we are able to identify the index of the particular note the user clicks in the list.

Inside the App function, we first set the sample notes list to the notes variable. Then we created three different functions to handle the add, edit, and delete button clicks.

Each function opens up the modal and passes in the necessary props we defined in the Modal component. Once the save or delete button is pressed, the notes list gets updated accordingly.

Finally, we rendered the title, subtitle of the application, added the Add button with the necessary props, and looped through the notes variable to display all the notes.

Everything is organized and there is not a single state variable for the modal itself, yet we are successfully handling three different modals.

At this point, you should have a working demo. Let's test it out!

Make sure your React app is still running in the terminal. If not, run npm start again. Now, open the browser and navigate to http://localhost:3000. You should be presented with a fully functional CRUD Notes demo app.

Add and Delete New Note

Conclusion

Although this might first seem like a basic notes app, we implemented all the functionality you would need to build a real-life CRUD application. We focused on the behavior and states, so make sure to adjust the content of modals based on your specific needs in the project.

Also, feel free to add some advanced input validation to the forms or write some backend so all the values are stored on the database and you do not lose your data. Currently, there are only console.log statements for the empty inputs and the data is stored in the state.

Because it is open-source, check out this GitHub repository and feel free to contribute any ideas or feature requests to the project to make it even better!


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket Sign Uphttps://www2.logrocket.com/react-performance-monitoring

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.

Top comments (0)