Create a Custom Context Menu Hook in React

Josh Cooter
Software Engineer
Read Time
7 min read
Published On
February 2, 2023

What are context menus?

When navigating an app on the web or mobile you may commonly come across custom menus with app-specific options that pop when right-clicking or tapping certain elements of the application. We call these context menus, and they are a type of user interface that can help to provide a guided experience to users to take specific actions depending on where that user action has taken place.

Project Structure

For our project, we will create a new React project—I prefer to use NextJS, but standard 'create-react-app' works just fine as well.

yarn create next-app context-menu

We are also using styled-components for styling in this project which you can install with:

yarn add styled-components

Note that if you are not using the latest version of NextJS and are having issues with fast refresh and your styling you might have to add a .babelrc file with the following:

{
    "presets": ["next/babel"],
    "plugins": [["styled-components", { "ssr": true }]]
}

We can then go in and remove all of the default content and styling and leave ourselves a classic “hello world" home page.

We’ll want to create a few extra directories inside of /src to keep our code organized:

  • /components will keep our reusable components separate from the code for our individual pages
  • /hooks will be home to the custom hook that we will write to handle the state and logic for our custom context menu

Creating a list

To showcase an example of where a context menu might be implemented, we will create a simple list of items that could mimic any number of elements in an application such as posts, orders, or anything else that might have a list of items where custom actions could be helpful.

First, we will create a List component in our /components directory. I prefer to keep all of my component related files together by creating directories for each of my components that contain the following:

  • index.ts exports my component so that I can import it directly from the component directory
  • List.component.tsx contains the code for my component itself
  • List.styles.ts contains all of my styling for the component
  • List.data.ts contains any static data necessary for the component

In a more involved project, I tend to store other component related files here as well such as StorybookJS stories, tests, etc.

First, we will create the data that we want to use for our list in List.data.ts:

// List.data.ts

export const menuData = [
    { id: 1, name: 'item 1' },
    { id: 2, name: 'item 2' },
    { id: 3, name: 'item 3' }
]

Then we will create our component itself, import that data, and map over it to create a list of items to work with:

// List.component.ts

import React from 'react'
import { menuData } from './List.data'
import { Container } from './List.styles'

export const List = () => {
    return (
        <>
            <Container>
                <ul>
                {menuData.map(item => (
                    <li>{item.name}</li>
                ))}
                </ul>
            </Container>
        </>
    )
}

You will notice that we have also imported a styled Container element from List.styles.ts which looks like this:

// List.styles.ts

import styled from 'styled-components'

export const Container = styled.div`

    li {
        background-color: lightgray;
        width: 50%;
        height: 50px;
        display: flex;
        flex-flow: row nowrap;
        justify-content: flex-start;
        align-items: center;
        list-style: none;
        border-radius: 6px;
        margin: 3px 0;
        padding-left: 6px;
    }

    li:hover {
        background-color: gray;
    }
`

If everything is correct we should have a simple list component to work with that has a bit of hover feedback.

Disabling the default right-click

Now that we have a list component in place, our first task is to disable the default right-click behavior that would typically pull up the system right-click menu so that we can replace it with our own context menu.

To accomplish this, we will use the onContextMenu prop and prevent the default behavior like so:

<Container onContextMenu={(e) => {
     e.preventDefault()
 }}>
     <ul>
        {menuData.map(item => (
           <li>{item.name}</li>
         ))}
     </ul>
 </Container>

You will notice when right-clicking our list you no longer see the system menu pop up. However, because we only prevented that default behavior for the containing div of our list component you can still right-click elsewhere in the app and pull up the system menu.

Creating our custom menu

Now we can create a new component called ContextMenu which will serve as our custom context menu that we provide for the user when they interact with our list.

First, we will create a ContextMenu.data.ts file to store the menu options:

export const menuData = [
    {
        id: 1,
        name: 'option 1'
    },
    {
        id: 2,
        name: 'option 2'
    },
    {
        id: 3,
        name: 'option 3'
    }
]

Then we will import that into a new ContextMenu.component.tsx file and map over it to create our list of options:

import React from 'react'
import { menuData } from './ContextMenu.data'
import { Container, MenuOption } from './ContextMenu.styles'

export const ContextMenu = () => {
    return (
        <>
        <Container>
            {menuData.map(item => (
                <MenuOption>{item.name}</MenuOption>
            ))}
        </Container>
        </>
    )
}

Then we will add styles to our MenuContext.styles.ts to give ourself something a little more visual:

import styled from 'styled-components'

export const Container = styled.div`
    width: 300px;
    background-color: darkgray;
    display: flex;
    flex-flow: column nowrap;
    justify-content: flex-start;
    align-items: center;
    border-radius: 3px;
`

export const MenuOption = styled.div`
    width: 100%;
    height: 30px;
    padding: 3px;

    &:hover {
        background-color: rgba(0, 0, 0, 0.3);
    }
`

If we add the component to our home page it should look something like this:

Implementing the useContextMenu hook

Now that we have a component to work with, we need to implement a custom hook to handle the logic and state for our context menu so that it can appear and disappear at the right location based on user interaction.

The first thing that we will do is implement a custom useContextMenu hook in our /hooks directory.

Here we will be using state to track whether or not the user has right-clicked (as well as if they have clicked elsewhere after right-clicking) and the coordinates of the user’s right-click interaction. We will also register an event listener in a useEffect to listen for that user’s clicks.

// useContextMenu.tsx

import React, { useEffect, useState } from 'react'

export const useContextMenu = () => {
    // boolean value to determine if the user has right clicked
    const [clicked, setClicked] = useState(false)
    // allows us to track the x,y coordinates of the users right click
    const [coords, setCoords] = useState({
        x: 0,
        y: 0
    })

    useEffect(() => {
        // reset clicked to false on user click
        const handleClick = () => {
            setClicked(false)
        }

        // add listener for user click
        document.addEventListener("click", handleClick)

        // clean up listener function to avoid memory leaks
        return () => {
            document.removeEventListener("click", handleClick);
          }
    }, [])

    return {
        clicked,
        setClicked,
        coords,
        setCoords
    }
}

This gives us all of the state and logic that we need to handle our custom context menu. Notice how we are sure to add a clean-up function to remove the click event listener at the end of the useEffect to prevent potential memory leaks.

Now that our custom hook is in place we can return to our List component. We first import and destructure the elements of our custom hook to be used as controls for our context menu. Then we once again target the onContextMenu prop to ensure we set the clicked state to true, and save the x and y coordinates of that click to be passed into our context menu as props.

Finally, we conditionally render the context menu based on whether or not our clicked state is true.

// List.component.tsx
import React from 'react'
import { menuData } from './List.data'
import { Container } from './List.styles'
import { useContextMenu } from '@/hooks/useContextMenu'
import { ContextMenu } from '../ContextMenu'

export const List = () => {
    // destructure our state and set state functions from our custom hook
    const { clicked, setClicked, coords, setCoords } = useContextMenu()
    
    return (
        <>
            <Container onContextMenu={(e) => {
                // prevent default right click behavior
                e.preventDefault()
                
                // set our click state to true when a user right clicks
                setClicked(true)

                // set the x and y coordinates of our users right click
                setCoords({ x: e.pageX, y: e.pageY })
            }}>
                {/* conditionally render the context menu if the clicked state is true
                and pass in the x and y coordinates of the right click as props */}
                {clicked && (
                    <ContextMenu top={coords.y} left={coords.x} />
                )}

                <ul>
                {menuData.map(item => (
                    <li key={item.id}>{item.name}</li>
                ))}
                </ul>
            </Container>
        </>
    )
}

To finish off we need to go to our ContextMenu component where we are receiving our coordinate props and pass those props to the Container styled component that we created so that we can use them to position our menu based on the user’s click coordinates.

// ContextMenu.component.tsx
import React from 'react'
import { menuData } from './ContextMenu.data'
import { Container, MenuOption } from './ContextMenu.styles'

export const ContextMenu = ({ top, left }: { top: number, left: number }) => {
    return (
        <>
        <Container top={top} left={left}>
            {menuData.map(item => (
                <MenuOption>{item.name}</MenuOption>
            ))}
        </Container>
        </>
    )
}

In the styles file we receive the coordinates as props and pass them as string interpolations so that the menu dynamically displays based on the user’s click coordinates.

// ContextMenu.styles.ts

import styled from 'styled-components'

export const Container = styled.div<{ top: number, left: number }>`
    width: 300px;
    background-color: darkgray;
    display: flex;
    flex-flow: column nowrap;
    justify-content: flex-start;
    align-items: center;
    border-radius: 3px;
    position: absolute;
    top: ${({top}) => `${top}px`};
    left: ${({left}) => `${left}px`};
`

export const MenuOption = styled.div`
    width: 100%;
    height: 30px;
    padding: 3px;

    &:hover {
        background-color: rgba(0, 0, 0, 0.3);
    }
`

If everything is done correctly our new menu should look something like this when right-clicking anywhere within the List component:

Conclusion

In this article we’ve covered all of the basics on how to set up and organize our project, create menu components, disable default browser right-click behavior, and utilize a custom hook to manage the state and logic. With this knowledge, you can create powerful custom context menu features and guided user interface experiences for your applications. Keep in mind that we did not cover mobile and dealing with touch-based interfaces, and so that is another consideration to weigh before implementing this on your next project.