How to create a right-click menu in React from scratch?
How to create a custom context menu in React?
In this article, we will create our own custom context menu with React hooks.
First things first, what's a context menu? It's a JavaScript event that gets fired by clicking the right mouse button.
Let's create a new app using Create React App and start building.
Create React App
Run the following command
npx create-react-app react-context-menu-app
We will create a folder component, clicking on which we'll show a Menu. Let's add a Folder component.
Create a Folder.js
file inside the components folder and add the following code.
// Folder.js
import ContextMenu from "./ContextMenu";
import "./Folder.css";
const Folder = () => {
return (
<div className="folder">
<p>Folder</p>
</div>
);
};
export default Folder;
And add these styles:
.folder {
width: 150px;
height: 30px;
border: 1px solid rgb(3, 150, 150);
margin: auto;
margin-top: 20%;
border-radius: 5px;
}
p {
margin: 0;
}
Call the Folder component on
App.js`.
// App.js
import "./App.css";
import Folder from "./components/Folder";
function App() {
return (
<div className="App">
<Folder />
</div>
);
}
export default App;
If you run your application, it should look like this.
Now we will build the context Menu
Building a Context Menu
Let's create a Context Menu component.
// ContextMenu.js
import "./ContextMenu.css";
const ContextMenu = () => {
return (
<ul className="contextMenu">
<li>Edit</li>
<li>Delete</li>
</ul>
) ;
};
export default ContextMenu;
Also, add the styles.
// ContextMenu.css
.contextMenu {
width: 200px;
background-color: rgb(3, 150, 150);
color: #fff;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
}
.contextMenu > li {
padding: 0.5rem 1rem;
border-bottom: 1px solid rgb(2, 87, 87);
}
.contextMenu > li {
padding: 0.5rem 1rem;
border-bottom: 1px solid rgb(2, 87, 87);
}
.contextMenu > li:hover {
cursor: pointer;
background-color: rgb(2, 87, 87);
}
You will get this:
There are a few problems at the moment.
- First, it is always visible.
- It renders only where we have placed it on the DOM
Let's fix the first one.
We will add an event listener for an event called contextmenu
and invoke a callback function to toggle the visibility of the menu.
Change the code in ContextMenu.js
to :
// ContextMenu.js
import { useState, useEffect } from "react";
import "./ContextMenu.css";
const ContextMenu = ({ folderRef }) => {
const [showMenu, setShowMenu] = useState(false);
const [xPos, setXPos] = useState(0);
const [yPos, setYPos] = useState(0);
const handleContextMenu = (e) => {
e.preventDefault();
if (folderRef?.current.contains(e.target)) {
setXPos(`${e.pageX}`);
setYPos(`${e.pageY}`);
setShowMenu(true);
} else {
setShowMenu(false);
}
};
const handleClick = () => {
setShowMenu(false);
};
useEffect(() => {
document.addEventListener("contextmenu", handleContextMenu);
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("contextmenu", handleContextMenu);
document.removeEventListener("click", handleClick);
};
}, []);
return showMenu ? (
<div style={{ position: "relative" }}>
<ul className="contextMenu" style={{ top: xPos, left: yPos }}>
<li>Edit</li>
<li>Delete</li>
</ul>
</div>
) : null;
};
export default ContextMenu;
Change the code in Folder.js
to:
// Folder.js
import React, { useRef } from "react";
import ContextMenu from "./ContextMenu";
import "./Folder.css";
const Folder = () => {
const folderRef = useRef(null);
return (
<div ref={folderRef} className="folder">
<p>Folder</p>
<ContextMenu folderRef={folderRef} />
</div>
);
};
export default Folder;
Let's understand the code.
Here we use useRef function that returns a mutable ref object whose .current property will contain a reference to the div element.
In Folder.js
file, we have add a useRef
function. It returns a mutable ref object whose .current
property contains a reference to the Folder
component.
In the ContextMenu.js
file, we have created a state showMenu
to toggle the visibility of ContextMenu.
And two more states to store the position of the click.
Then, we have added a callback function handleContextMenu
that is invoked whenever a user right-clicks and updates the state for showMenu.
We are checking if the click was triggered inside the
folderRef` only then we will store click's position & show the Menu. Else, we will hide the context Menu.
And another event listener to listen for click
event to hide the menu.
Note: We have added e.preventDefault
to prevent browser's default context menu from running.
Also, it's a good practice to write a cleanup function in useEffect whenever we attach an event listener to prevent any memory leaks.
And, in the JSX, we are conditionally rendering the Menu
.
Now, we are able to show the menu on right click & get the context menu.