Композиция хуков

Композиция хуков в библиотеке React Aria позволяет создавать сложные интерактивные компоненты, объединяя низкоуровневые хуки для управления состоянием, фокусом, клавиатурной навигацией и доступностью. React Aria не ограничивается только готовыми компонентами — её сила в возможности комбинировать хуки, создавая собственные, полностью контролируемые элементы интерфейса.


Принцип композиции

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

  • useButton — управление клавиатурным и мышиным взаимодействием кнопки.
  • useFocusRing — отслеживание фокуса и визуальных индикаторов для доступности.
  • useHover — реакция на наведение мыши.
  • useOverlayTrigger и useOverlay — управление модальными или всплывающими окнами.
  • useListBox и useOption — создание списков с поддержкой клавиатуры и ARIA-ролей.

Главная задача — комбинировать хуки так, чтобы их состояния не дублировались и не перезаписывали друг друга. Для этого каждый хук возвращает props, которые нужно корректно объединять с помощью mergeProps из React Aria Utilities:

import { mergeProps } from '@react-aria/utils';

const combinedProps = mergeProps(buttonProps, hoverProps, focusProps);

mergeProps гарантирует, что обработчики событий и атрибуты ARIA будут объединены без конфликтов.


Сценарий: интерактивная кнопка

Для примера создадим кнопку с поддержкой фокуса, наведения и клавиатурного взаимодействия:

import { useButton } from '@react-aria/button';
import { useFocusRing } from '@react-aria/focus';
import { useHover } from '@react-aria/interactions';

function AccessibleButton(props) {
  const { buttonProps } = useButton(props);
  const { isFocusVisible, focusProps } = useFocusRing();
  const { hoverProps, isHovered } = useHover();

  const combinedProps = mergeProps(buttonProps, focusProps, hoverProps);

  return (
    <button {...combinedProps} style={{
      outline: isFocusVisible ? '2px solid blue' : 'none',
      backgroundColor: isHovered ? '#eee' : '#fff'
    }}>
      {props.children}
    </button>
  );
}

Разбор композиции:

  • useButton обрабатывает нажатия и клавиатурные события (Enter, Space).
  • useFocusRing добавляет визуальный индикатор фокуса при навигации с клавиатуры.
  • useHover отслеживает состояние наведения и позволяет динамически изменять стиль.
  • mergeProps объединяет события и атрибуты всех хуков без перезаписи.

Сложная композиция: раскрывающийся список

В случае компонентов вроде Select или Menu, требуется объединить несколько хуков:

  1. useListBox — для списка опций с ARIA-ролями и клавиатурной навигацией.
  2. useOption — для каждой опции, чтобы управлять выделением и состоянием фокуса.
  3. useOverlayTrigger и useOverlay — для управления открытием/закрытием списка.
  4. useFocusRing — для визуального фокуса.
  5. useHover — для наведения на опции.

Пример интеграции хуков для одной опции списка:

import { useOption } from '@react-aria/listbox';
import { useFocusRing } from '@react-aria/focus';
import { useHover } from '@react-aria/interactions';
import { mergeProps } from '@react-aria/utils';

function Option({ item, state }) {
  const { optionProps, isSelected, isDisabled } = useOption({ key: item.key }, state);
  const { isFocusVisible, focusProps } = useFocusRing();
  const { hoverProps, isHovered } = useHover();

  const combinedProps = mergeProps(optionProps, focusProps, hoverProps);

  return (
    <div
      {...combinedProps}
      style={{
        backgroundColor: isSelected ? 'lightblue' : isHovered ? '#eee' : '#fff',
        outline: isFocusVisible ? '2px solid blue' : 'none',
        opacity: isDisabled ? 0.5 : 1,
        padding: '4px 8px',
        cursor: isDisabled ? 'not-allowed' : 'pointer'
      }}
    >
      {item.label}
    </div>
  );
}

Здесь каждый хук отвечает за свой аспект поведения:

  • useOption — управление выделением и ролями ARIA.
  • useFocusRing — визуальный индикатор фокуса.
  • useHover — реакция на наведение мыши.

Ключевой момент — использование mergeProps предотвращает конфликт обработчиков onClick, onKeyDown, onMouseEnter и onMouseLeave.


Советы по эффективной композиции

  1. Разделение ответственности: каждый хук должен отвечать только за один аспект поведения.
  2. Использование mergeProps: объединение props обязательное при комбинировании нескольких хуков.
  3. Следование ARIA-стандартам: хуки React Aria автоматически назначают правильные роли, атрибуты и свойства, важно их сохранять при композиции.
  4. Минимизация пересечений состояния: состояние фокуса, наведения и выбора должно храниться отдельно в соответствующих хуках.
  5. Создание кастомных хуков: для повторно используемых комбинаций можно обернуть несколько хуков в собственный.

Пример кастомного хука: кнопка с подсказкой

import { useButton } from '@react-aria/button';
import { useFocusRing } from '@react-aria/focus';
import { useHover } from '@react-aria/interactions';
import { mergeProps } from '@react-aria/utils';
import { useState } from 'react';

function useTooltipButton(props) {
  const { buttonProps } = useButton(props);
  const { isFocusVisible, focusProps } = useFocusRing();
  const { hoverProps, isHovered } = useHover();
  const [showTooltip, setShowTooltip] = useState(false);

  const combinedProps = mergeProps(buttonProps, focusProps, hoverProps, {
    onMouseEnter: () => setShowTooltip(true),
    onMouseLeave: () => setShowTooltip(false)
  });

  return { combinedProps, isHovered, isFocusVisible, showTooltip };
}

Такой хук позволяет многократно использовать комбинацию поведения кнопки с подсказкой, не дублируя код.


Композиция хуков в React Aria обеспечивает максимальную гибкость и контроль, позволяя создавать доступные и интерактивные интерфейсы с минимальным количеством конфликтов и чистой архитектурой компонентов.