📅 3 years ago 🕒 17 min read 🙎♂️ by Madza
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 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.
For reference here is the source code of the final project.
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 whole app.
The main advantage of the utility is the 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 from 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 the 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, antd or chakra).
We will first create a React app by using create-react-app. It is a popular utility designed to scaffold the fully functional React app in a minute or less with zero configuration.
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 is created in your current working directory.
Next, change the directory by cd crud-notes
and 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, find the files App.js
, App.css
, index.js
, and remove the content from them since we will write everything from scratch. Also, rename App.css
to styles.css
and remove the index.css
file as we will not need it.
In the newly renamed styles.css
include the following style rules:
@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 { margin: 0 auto; text-align: center; }
First, we declared some reset rules to margin
, padding
, and border-box
, so all the elements are being displayed equally in all browsers. We also made sure the app uses Motserrat font.
Then we added some padding
to the body, set the app wrapper to never exceed 500px
, centered it in the viewport as well as centered the text inside it.
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 nice-modal-react
throughout the whole app, we will also need to set up a separate provider that will use React context to control the state globally.
To do that, open the index.js
file that is the root file to render the whole application, import the NiceModal
and wrap it around the <App />
component:
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.
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: mkdir components && cd components && touch Modal.js Modal.css Button.js Button.css Note.js Note.css
.
Open the Modal.js
file and include the following code:
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>{e}</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 actual 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 the 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 respectively. 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 can not 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 the Modal.css
file and include the following style rules:
.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 some padding
, specific width
, border-radius
, centered the text, set the background-color
to be white, to give a nice contrast to the background, 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
, 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.
Open the Button.js
file and include the following code:
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 name
, backgroundColor
and onClick
props once imported and used in App.js
.
Open the Button.css
file and include the following style rules:
.button { border: none; padding: 5px 10px; cursor: pointer; border-radius: 5px; width: 100%; }
We removed the default button border
, added some padding
, set cursor
to be a pointer, added some border-radius
for smooth corners, and set the button to fill all the available width
.
Open the Note.js
file and include the following code:
import "./Note.css"; import Button from "./Button"; const Note = ({ titlnote, onClickEdit, onClickDelete }) => { return ( <div className="note"> <p>{titlnote}</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 that we will pass in when we import and use the Note
component in the App.js
.
Open the Note.css
file and include the following style rules:
.note { max-width: 500px; 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 3-column layout with a 20px
gap between, where 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 being automatically split.
We also created some media rules for responsiveness. For the screen widths 400px
and smaller, the note will switch to the 1 column layout meaning that all the included elements (title, edit button, and delete button) will be shown directly below each other.
Now lets put everything together and create a logic for our app. Open the App.js
file and include the following code:
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: "limegreen" }).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 permamently 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="limegreen" 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. We also imported the NiceModal
component as well as every individual component we created in the previous phase. To style the component we will use an external stylesheet we created while setting up the React app.
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.
At this point, you should have a working demo.
Everything is organized and there is not a single state variable for the modal itself, yet we are successfully handling three different modals.
Make sure your React app is still running in the terminal. If it is not run npm start
command again. Now open the browser and navigate to http://localhost:3000
and you should be presented with a fully functional CRUD Notes demo app.
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 mainly focused just 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 their GitHub repository and feel free to contribute any ideas or feature requests to the project to make it even better!