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 onApp.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.

folderC.png

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:

menu2.png

There are a few problems at the moment.

  1. First, it is always visible.
  2. 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 thefolderRef` 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.