18.03.25

Контекстное меню в React: лучший подход

10 мин · Обучающие


Введение


Контекстное меню — это всплывающее меню, которое появляется при нажатии правой кнопки мыши. В браузерах по умолчанию открывается стандартное меню, но в веб-приложениях часто нужно создавать свое, кастомное. В этой статье разберем, как это сделать в React.



Почему нельзя просто использовать onContextMenu?


Метод onContextMenu позволяет отловить правый клик, но у него есть несколько проблем:


  • Ограниченное управление позиционированием — сложно точно задать место появления меню.
  • Меню может выйти за границы экрана — пользователь не увидит часть пунктов.
  • Ограниченная кастомизация — сложно изменить стиль и добавить новые функции.


Чтобы решить эти проблемы, создадим своё контекстное меню с полным контролем над его поведением.


Шаг 1: Создаем хук useContextMenu


Начнем с создания хука, который управляет состоянием меню: его видимостью, позицией и содержимым.


import { useState, useCallback } from "react";

export interface ContextMenuHandlers {
    open: (x: number, y: number, record: any) => void;
    close: () => void;
}

export function useContextMenu(handlerRef: React.RefObject<ContextMenuHandlers>) {
    // Состояние меню: открыто или нет, координаты и данные записи
    const [state, setState] = useState({
        visible: false,
        x: 0,
        y: 0,
        record: null,
    });

    // Функция открытия меню
    const open = (x: number, y: number, record: any) => {
        const menuWidth = 150; // Ширина меню
        const menuHeight = 100; // Высота меню
        const maxX = window.innerWidth - menuWidth;
        const maxY = window.innerHeight - menuHeight;

        // Устанавливаем новое состояние
        setState({
            visible: true,
            x: Math.min(x, maxX), // Ограничиваем координаты
            y: Math.min(y, maxY),
            record,
        });
    };

    // Функция закрытия меню
    const close = () => {
        setState((prev) => ({ ...prev, visible: false }));
    };

    // Связываем методы с handlerRef, если он передан
    if (handlerRef.current) {
        handlerRef.current.open = open;
        handlerRef.current.close = close;
    }

    return state;
}


Как работает этот хук?


  1. Используем useState, чтобы хранить:
    • visible — отображается ли меню
    • x и y — координаты меню
    • record — данные записи, на которой был клик
  2. Функция open()
    • Принимает координаты клика и данные записи
    • Вычисляет, не выходит ли меню за границы экрана
    • Обновляет состояние меню
  3. Функция close()
    • Просто скрывает меню
  4. Если передан handlerRef, то записываем в него функции open и close, чтобы их можно было вызвать извне


Шаг 2: Создаем компонент ContextMenu


Теперь напишем сам компонент меню:


import React from "react";
import "./ContextMenu.scss";
import { ContextMenuHandlers, useContextMenu } from "../../hooks/useContextMenu";

interface ContextMenuProps {
    handlerRef: React.RefObject<ContextMenuHandlers>;
    onOpenNewTab: (record: any) => void;
}

export default function ContextMenu({ handlerRef, onOpenNewTab }: ContextMenuProps) {
    const { visible, x, y, record } = useContextMenu(handlerRef);

    // Функция для открытия записи в новой вкладке
    const handleNewTab = () => {
        if (record) {
            onOpenNewTab(record);
        }
    };

    if (!visible) return null; // Если меню скрыто, ничего не рендерим

    return (
        <div
            className="context-menu"
            style={{ top: y, left: x }}
            onMouseLeave={() => handlerRef.current?.close()}
        >
            <div className="context-menu-item" onClick={handleNewTab}>
                Открыть в новой вкладке
            </div>
            <div className="context-menu-item" onClick={() => handlerRef.current?.close()}>
                Отмена
            </div>
        </div>
    );
}


Что здесь происходит?


  1. Получаем данные о меню из хука useContextMenu
  2. Определяем функцию handleNewTab(), которая открывает запись в новой вкладке
  3. Если меню скрыто (!visible), то ничего не отображаем
  4. Рендерим контейнер меню и два пункта:
    • "Открыть в новой вкладке"
    • "Отмена"
  5. Если курсор уходит за пределы меню (onMouseLeave), меню автоматически закрывается



Шаг 3: Добавляем обработчик handleContextMenu


Теперь нужно подключить наше контекстное меню к списку данных. Добавим обработчик:


const handleContextMenu = (event: React.MouseEvent, record: any) => {
    event.preventDefault(); // Отменяем стандартное меню браузера
    contextMenuRef.current?.open(event.clientX, event.clientY, record);
};


Как это работает?


  1. event.preventDefault() — отключает стандартное контекстное меню
  2. contextMenuRef.current?.open() — открывает наше меню в нужном месте


Готовые библиотеки


Если не хочется писать код с нуля, можно использовать готовые решения:


  • @szhsin/react-menu — мощный UI-компонент с анимацией.
  • radix-ui/menu — доступный и кастомизируемый вариант.


UI-советы


  • Не давайте меню выходить за границы экрана — ограничьте координаты.
  • Добавляйте иконки для пунктов — пользователи быстрее ориентируются.
  • Используйте анимации появления — меню будет выглядеть плавнее.
  • Продумайте логику закрытия — при клике вне меню или на Esc оно должно скрываться.


Итог


Мы создали удобное контекстное меню с учетом границ экрана и интеграцией в компонент с данными. Также рассмотрели альтернативные библиотеки и лучшие UX-практики. Теперь вы можете легко внедрить контекстное меню в свой React-проект!