Разделение ответственности — фундаментальный принцип проектирования интерфейсов, при котором разные аспекты поведения, логики и визуального отображения изолируются друг от друга. Такой подход повышает читаемость кода, упрощает сопровождение, улучшает тестируемость и позволяет переиспользовать логику независимо от конкретного пользовательского интерфейса.
В экосистеме React этот принцип проявляется в виде разделения:
Библиотека React Aria строится именно вокруг такого подхода. Она отделяет сложную логику доступных интерфейсных паттернов от визуальной реализации компонентов. Это означает, что библиотека не предоставляет готовые стилизованные UI-компоненты — она предоставляет поведенческую модель, которая затем может быть применена к любой структуре DOM.
Экосистема React Aria фактически состоит из двух логических уровней:
Предоставляется через React Aria hooks. Эти хуки:
Пример:
useButton
useListBox
useMenu
useComboBox
Каждый хук инкапсулирует поведенческий контракт компонента.
Предоставляется библиотекой React Stately.
Этот слой отвечает за:
Пример:
useListState
useComboBoxState
useTreeState
Архитектурно взаимодействие выглядит так:
UI слой (JSX + стили)
↓
React Aria (поведение)
↓
React Stately (состояние)
Каждый слой решает свою отдельную задачу.
В традиционных UI-библиотеках часто используется противоположный подход: компоненты поставляются полностью готовыми.
Пример типичного компонента:
<Button variant="primary">Submit</Button>
Внутри такого компонента скрыто:
Этот подход имеет ряд недостатков:
Ограниченная кастомизация
Изменить структуру DOM невозможно.
Сложность переиспользования логики
Поведенческую часть нельзя применить в другом UI.
Зависимость от CSS библиотеки
Стили жестко связаны с компонентами.
React Aria предлагает headless-архитектуру.
Headless означает, что библиотека предоставляет:
но не навязывает разметку и стили.
Рассмотрим кнопку.
Хук:
useButton
Он реализует:
Разметка остается полностью под контролем разработчика.
Пример:
import {useButton} from 'react-aria';
import {useRef} from 'react';
function Button(props) {
let ref = useRef();
let {buttonProps} = useButton(props, ref);
return (
<button {...buttonProps} ref={ref}>
{props.children}
</button>
);
}
| Слой | Ответственность |
|---|---|
| JSX | структура DOM |
| CSS | стили |
| React Aria | поведение |
| React Stately | состояние |
Каждый хук React Aria реализует контракт взаимодействия, описанный в спецификациях доступных компонентов WAI-ARIA.
Например:
Кнопка должна поддерживать
Enter и SpaceВместо того чтобы реализовывать это вручную, используется готовый хук.
UI-компоненты обычно обрабатывают множество типов взаимодействия:
React Aria выделяет эту логику в отдельные утилиты:
usePress
useHover
useFocus
useKeyboard
Каждая из них отвечает за конкретный тип взаимодействия.
Этот хук объединяет разные способы активации элемента:
Пример:
import {usePress} from 'react-aria';
function Pressable(props) {
let {pressProps} = usePress({
onPress: props.onPress
});
return (
<div {...pressProps}>
{props.children}
</div>
);
}
Здесь:
usePress отвечает за взаимодействиеdiv отвечает за отображениеReact Aria не диктует структуру DOM.
Например, список может быть реализован через:
ul/li
div/div
table
grid
Хуки лишь добавляют необходимые:
Большинство сложных компонентов имеют внутреннее состояние.
Примеры:
React Aria не хранит состояние — оно вынесено в React Stately.
Компонент списка использует два слоя.
let state = useListState(props);
Состояние содержит:
let {listBoxProps} = useListBox(props, state, ref);
Этот хук добавляет:
<ul {...listBoxProps} ref={ref}>
{[...state.collection].map(item => (
<Option key={item.key} item={item} state={state} />
))}
</ul>
| Слой | Реализация |
|---|---|
| состояние | useListState |
| поведение | useListBox |
| отображение | JSX |
React Aria активно использует композицию хуков.
Компонент может комбинировать несколько поведений.
Пример:
useButton
useHover
useFocusRing
Каждый из них добавляет свою часть функциональности.
function FancyButton(props) {
let ref = useRef();
let {buttonProps} = useButton(props, ref);
let {hoverProps, isHovered} = useHover(props);
let {focusProps, isFocusVisible} = useFocusRing();
return (
<button
{...buttonProps}
{...hoverProps}
{...focusProps}
ref={ref}
>
{props.children}
</button>
);
}
Каждый хук решает отдельную задачу.
Благодаря разделению ответственности одна и та же логика может применяться к разным элементам.
Например, usePress можно использовать для:
button
div
span
custom components
Это делает систему крайне гибкой.
Сложные интерфейсные паттерны содержат десятки правил взаимодействия.
Примеры:
Каждый из этих компонентов требует:
React Aria выносит эти правила в отдельные хуки.
ComboBox сочетает в себе:
В традиционной реализации всё это находится внутри одного компонента.
В React Aria логика распределяется.
useComboBoxState
Отвечает за:
useComboBox
Реализует:
Полностью свободная JSX-разметка.
Разделение ответственности значительно упрощает тестирование.
Можно тестировать:
useListState
usePress
useButton
через snapshot-тесты или визуальные тесты.
Некоторые аспекты UI-поведения крайне сложны:
React Aria изолирует эти алгоритмы внутри хуков.
Разработчику не нужно реализовывать их вручную.
В больших приложениях число компонентов быстро растет.
Без разделения ответственности возникают проблемы:
React Aria позволяет строить иерархию абстракций:
Primitive hooks
↓
Headless components
↓
Design system components
↓
Application UI
React Aria идеально подходит для создания дизайн-систем.
Типичная архитектура:
React Aria hooks.
Например:
ButtonBase
ListBoxBase
MenuBase
PrimaryButton
IconButton
DropdownMenu
Разделение ответственности позволяет использовать одну и ту же логику:
Поскольку React Aria не содержит CSS, визуальный слой может полностью меняться.
Без разделения ответственности компонент может выглядеть так:
500–1000 строк кода
Внутри:
После разделения:
UI компонент — 50–100 строк
Большая часть логики уже инкапсулирована в хуках.
Когда поведение вынесено в отдельные хуки:
Библиотека не предоставляет стилизованные компоненты.
Хуки отвечают за взаимодействие.
React Stately управляет состоянием.
Мелкие хуки объединяются в более сложные.
Разметка и стили не ограничены библиотекой.
Application UI
↓
Design System Components
↓
Headless Components
↓
React Aria (behavior)
↓
React Stately (state)
↓
DOM
Такая модель делает интерфейсные компоненты: