Библиотека React Aria построена вокруг идеи композиции. Отдельные хуки предоставляют поведенческую логику для конкретных интерфейсных паттернов — кнопок, списков, диалогов, полей ввода, выпадающих меню. Однако на практике элементы интерфейса редко используются изолированно. Реальные интерфейсы состоят из взаимосвязанных компонентов, каждый из которых управляет собственной логикой доступности, фокусировки, состояния и взаимодействия.
Совместное использование хуков React Aria позволяет:
Благодаря архитектуре библиотеки хуки можно комбинировать без конфликтов, поскольку каждый из них возвращает набор prop-объектов, предназначенных для передачи в DOM-элементы.
Хуки React Aria возвращают объекты с пропсами:
{
buttonProps,
inputProps,
labelProps,
menuProps
}
Если компонент использует несколько хуков, возникает необходимость объединить их пропсы.
Для этого используется утилита mergeProps.
import {mergeProps} from '@react-aria/utils';
const props = mergeProps(buttonProps, focusProps);
Функция корректно объединяет:
Это предотвращает перезапись обработчиков.
Часто один элемент интерфейса выполняет несколько ролей одновременно.
Например:
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 разделяет:
Сложные компоненты обычно используют оба слоя.
Компонент меню использует:
useMenuTriggeruseMenuuseMenuItemuseTreeStateButton
└─ 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-*При композиции важно:
props, возвращаемые хукамиmergePropsИначе доступность может нарушиться.
При разработке крупных интерфейсов на React Aria часто используются следующие паттерны.
Compound components
<Tabs>
<TabList>
<Tab />
</TabList>
<TabPanels>
<TabPanel />
</TabPanels>
</Tabs>
Headless components
Компоненты без стилей, содержащие только поведение.
State lifting
Состояние хранится на верхнем уровне.
Context sharing
Состояние передается через React Context.
Композиционная архитектура дает несколько ключевых преимуществ:
Гибкость
Хуки можно комбинировать в любых сочетаниях.
Переиспользуемость
Одна и та же логика применяется в разных компонентах.
Чистая архитектура
Состояние, поведение и визуальный слой разделены.
Доступность
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 из набора хуков в полноценную основу для архитектуры интерфейсов.