Improve modal management in React with nice-modal-react
Tags: javascript
Published: 2022-01-14 17:44:34

**Written by [Madars Bišs](https://blog.logrocket.com/author/madarsbiss/)✏️**

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](https://twitter.com), 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](https://github.com/eBay/nice-modal-react), which is a useful modal utility for [React](https://reactjs.org) created by the developer team of [eBay](https://ebay.com). 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](https://blog.logrocket.com/wp-content/uploads/2022/01/crud-notes.png)

For reference, here is the [source code](https://codesandbox.io/s/nice-modal-react-182gc) of the final project.

## Why use nice-modal-react?

The [nice-modal-react](https://github.com/eBay/nice-modal-react) package is a zero-dependency utility written in [TypeScript](https://www.typescriptlang.org/) 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](https://github.com/eBay/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](https://mui.com/), [Ant design](https://ant.design/), or [Chakra](https://chakra-ui.com/)).

## Initializing a React app

We will first create a React app by using [Create React App](https://create-react-app.dev/). 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:

```JSX
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;
}
```

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](https://reactjs.org/docs/context.html) 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:

```JSX
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
);
```

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:

```JSX
bash
mkdir components && cd components && touch Modal.js Modal.css Button.js Button.css Note.js Note.css
```

### Modal

Open `Modal.js` and include the following code:

```JSX
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;
```

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:

```JSX
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;
}
```

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:

```JSX
javascript
import "./Button.css";

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

export default Button;
```

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:

```JSX
css
.button {
  border: none;
  padding: 5px 10px;
  cursor: pointer;
  border-radius: 5px;
  width: 100%;
}
```

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:

```JSX
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;
```

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:

```JSX
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;
  }
}
```

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:

```JSX
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>
  );
}
```

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](https://blog.logrocket.com/wp-content/uploads/2022/01/add-new-note.gif)

## 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](https://github.com/eBay/nice-modal-react) 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](https://www2.logrocket.com/react-performance-monitoring). 

![LogRocket Sign Up](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)https://www2.logrocket.com/react-performance-monitoring

[LogRocket](https://www2.logrocket.com/react-performance-monitoring) 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](https://www2.logrocket.com/react-performance-monitoring).