Совместное использование

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

Совместное использование хуков React Aria позволяет:

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

Благодаря архитектуре библиотеки хуки можно комбинировать без конфликтов, поскольку каждый из них возвращает набор prop-объектов, предназначенных для передачи в DOM-элементы.


Основной принцип: объединение prop-объектов

Хуки React Aria возвращают объекты с пропсами:

{
  buttonProps,
  inputProps,
  labelProps,
  menuProps
}

Если компонент использует несколько хуков, возникает необходимость объединить их пропсы.

Для этого используется утилита mergeProps.

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

const props = mergeProps(buttonProps, focusProps);

Функция корректно объединяет:

  • обработчики событий
  • aria-атрибуты
  • свойства DOM

Это предотвращает перезапись обработчиков.


Совместное использование поведенческих хуков

Часто один элемент интерфейса выполняет несколько ролей одновременно.

Например:

  • кнопка с управлением фокусом
  • поле ввода с обработкой клавиатуры
  • интерактивный список с навигацией стрелками

Пример: кнопка с отслеживанием фокуса

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

function Button(props) {
  let ref = React.useRef();

  let {buttonProps} = useButton(props, ref);
  let {focusProps, isFocusVisible} = useFocusRing();

  return (
    <button
      {...mergeProps(buttonProps, focusProps)}
      ref={ref}
      className={isFocusVisible ? "focus-visible" : ""}
    >
      {props.children}
    </button>
  );
}

Здесь используются два хука:

Хук Назначение
useButton логика кнопки и aria-атрибуты
useFocusRing управление стилем фокуса

Оба набора пропсов объединяются через mergeProps.


Совместное использование состояния и поведения

React Aria разделяет:

  • поведение — React Aria
  • состояние — React Stately

Сложные компоненты обычно используют оба слоя.

Пример: выпадающее меню

Компонент меню использует:

  • useMenuTrigger
  • useMenu
  • useMenuItem
  • useTreeState
Button
  └─ Menu
        ├─ MenuItem
        ├─ MenuItem
        └─ MenuItem

Состояние хранится в useTreeState, а поведенческая логика распределена между несколькими хуками.

let state = useTreeState(props);
let {menuTriggerProps, menuProps} = useMenuTrigger({}, state, ref);

Такое разделение позволяет повторно использовать одну и ту же логику состояния для различных интерфейсов.


Разделение компонентов на уровни

При совместном использовании React Aria компоненты обычно делятся на три уровня:

1. Контейнер состояния

Хранит состояние и бизнес-логику.

useTreeState
useListState
useComboBoxState

2. Поведенческие хуки

Добавляют доступность и взаимодействие.

useMenu
useListBox
useButton
useDialog

3. Визуальные компоненты

Отвечают только за разметку и стили.

<div>
<ul>
<li>

Такая архитектура обеспечивает высокую переиспользуемость.


Совместное использование через контекст

В сложных компонентах состояние часто передается через React Context.

Например:

ComboBox
 ├─ Input
 ├─ ListBox
 └─ Popover

Все эти части должны использовать одно состояние.

Создается контекст:

const ComboBoxContext = React.createContext();

Компонент верхнего уровня:

function ComboBox(props) {
  let state = useComboBoxState(props);

  return (
    <ComboBoxContext.Provider value={state}>
      {props.children}
    </ComboBoxContext.Provider>
  );
}

Внутренние элементы получают состояние:

let state = React.useContext(ComboBoxContext);

Это позволяет разделить логику между несколькими компонентами.


Совместное использование с рефами

Многие хуки React Aria требуют ref для работы с DOM-элементом.

Пример:

useButton(props, ref)

Если компонент использует несколько хуков, все они могут использовать один и тот же ref.

let ref = React.useRef();

let {buttonProps} = useButton(props, ref);
let {hoverProps} = useHover();

Реф передается в DOM:

<button ref={ref} {...mergeProps(buttonProps, hoverProps)} />

Композиция списков и элементов

Компоненты коллекций — списки, меню, таблицы — используют один общий механизм.

useCollection
useTreeState
useListBox
useOption

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

Пример элемента списка

function Option({item, state}) {
  let ref = React.useRef();

  let {optionProps, isSelected} = useOption(
    {key: item.key},
    state,
    ref
  );

  return (
    <li
      {...optionProps}
      ref={ref}
      className={isSelected ? "selected" : ""}
    >
      {item.rendered}
    </li>
  );
}

Родительский список:

function ListBox(props) {
  let state = useListState(props);
  let ref = React.useRef();

  let {listBoxProps} = useListBox(props, state, ref);

  return (
    <ul {...listBoxProps} ref={ref}>
      {[...state.collection].map(item => (
        <Option key={item.key} item={item} state={state} />
      ))}
    </ul>
  );
}

Здесь состояние используется одновременно:

  • списком
  • элементами списка

Совместное использование событий

При объединении нескольких хуков важно корректно объединять обработчики событий.

mergeProps автоматически объединяет:

onClick
onKeyDown
onFocus
onBlur

Пример:

let {buttonProps} = useButton(props, ref);
let {hoverProps} = useHover();

<button {...mergeProps(buttonProps, hoverProps)} />

Если события объединять вручную, часть логики может потеряться.


Комбинирование интерактивных состояний

Многие хуки возвращают флаги состояния.

Например:

isPressed
isHovered
isFocused
isSelected

Компонент может комбинировать их для управления стилями.

let {buttonProps, isPressed} = useButton(props, ref);
let {hoverProps, isHovered} = useHover();
let {focusProps, isFocusVisible} = useFocusRing();

Использование:

<button
  {...mergeProps(buttonProps, hoverProps, focusProps)}
  className={`
    button
    ${isPressed ? "pressed" : ""}
    ${isHovered ? "hovered" : ""}
    ${isFocusVisible ? "focused" : ""}
  `}
/>

Так создаются сложные интерактивные состояния.


Композиция сложных компонентов

Реальные интерфейсы часто состоят из нескольких связанных элементов.

Пример: Select

Select
 ├─ Button
 ├─ Popover
 └─ ListBox

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

Компонент Хук
Select useSelect
Button useButton
Popover useOverlay
ListBox useListBox

Все они используют одно состояние:

useSelectState

Так достигается синхронизация между частями интерфейса.


Переиспользование логики в собственных компонентах

Совместное использование хуков позволяет создавать собственные абстракции.

Например, кастомный компонент:

IconButton

Он может объединять:

useButton
useHover
useFocusRing
function IconButton(props) {
  let ref = React.useRef();

  let {buttonProps} = useButton(props, ref);
  let {hoverProps} = useHover();
  let {focusProps} = useFocusRing();

  return (
    <button
      {...mergeProps(buttonProps, hoverProps, focusProps)}
      ref={ref}
      className="icon-button"
    >
      {props.icon}
    </button>
  );
}

Такие компоненты становятся строительными блоками интерфейса.


Управление доступностью при композиции

React Aria гарантирует корректную доступность даже при объединении нескольких хуков.

Это достигается за счет:

  • правильного использования aria-*
  • управления фокусом
  • поддержки клавиатурной навигации
  • соблюдения WAI-ARIA паттернов

При композиции важно:

  1. передавать все props, возвращаемые хуками
  2. использовать mergeProps
  3. не удалять aria-атрибуты

Иначе доступность может нарушиться.


Типичные архитектурные паттерны

При разработке крупных интерфейсов на React Aria часто используются следующие паттерны.

Compound components

<Tabs>
  <TabList>
    <Tab />
  </TabList>

  <TabPanels>
    <TabPanel />
  </TabPanels>
</Tabs>

Headless components

Компоненты без стилей, содержащие только поведение.

State lifting

Состояние хранится на верхнем уровне.

Context sharing

Состояние передается через React Context.


Преимущества совместного использования React Aria

Композиционная архитектура дает несколько ключевых преимуществ:

Гибкость

Хуки можно комбинировать в любых сочетаниях.

Переиспользуемость

Одна и та же логика применяется в разных компонентах.

Чистая архитектура

Состояние, поведение и визуальный слой разделены.

Доступность

ARIA-паттерны автоматически соблюдаются.

Масштабируемость

Компоненты легко расширяются без переписывания логики.


Распространённые ошибки при совместном использовании

Перезапись props

Неправильное объединение пропсов может удалить обработчики событий.

Отсутствие ref

Некоторые хуки требуют реф для управления фокусом.

Неправильное разделение состояния

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

Удаление aria-атрибутов

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


Организация кода в крупных проектах

При работе с React Aria в больших проектах структура обычно выглядит так:

components/
  button/
    Button.tsx
    IconButton.tsx

  menu/
    Menu.tsx
    MenuItem.tsx

  listbox/
    ListBox.tsx
    Option.tsx

hooks/
  useFocusStyles.ts
  useInteractiveStates.ts

Собственные хуки часто оборачивают несколько хуков React Aria.

Это упрощает повторное использование и уменьшает количество дублирующегося кода.


Масштабирование через собственные хуки

Можно создавать собственные композиционные хуки.

Пример:

function useInteractiveButton(props, ref) {
  let {buttonProps, isPressed} = useButton(props, ref);
  let {hoverProps, isHovered} = useHover();
  let {focusProps, isFocusVisible} = useFocusRing();

  return {
    props: mergeProps(buttonProps, hoverProps, focusProps),
    isPressed,
    isHovered,
    isFocusVisible
  };
}

Использование:

let {props, isPressed} = useInteractiveButton(props, ref);

Так создаются единые стандарты интерактивности во всем приложении.


Роль композиции в архитектуре React Aria

React Aria изначально проектировалась как набор атомарных поведенческих модулей. Именно возможность свободно комбинировать эти модули делает библиотеку пригодной для построения:

  • дизайн-систем
  • корпоративных UI-библиотек
  • масштабируемых интерфейсов
  • полностью кастомных компонентов

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