Разделение ответственности

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

В экосистеме React этот принцип проявляется в виде разделения:

  • логики поведения компонентов
  • доступности (accessibility)
  • визуального представления
  • управления состоянием

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


Два уровня архитектуры React Aria

Экосистема React Aria фактически состоит из двух логических уровней:

Поведенческий слой

Предоставляется через React Aria hooks. Эти хуки:

  • реализуют взаимодействие пользователя
  • обеспечивают поддержку клавиатуры
  • добавляют ARIA-атрибуты
  • реализуют фокус-менеджмент
  • обеспечивают совместимость со screen readers

Пример:

useButton
useListBox
useMenu
useComboBox

Каждый хук инкапсулирует поведенческий контракт компонента.


Слой управления состоянием

Предоставляется библиотекой React Stately.

Этот слой отвечает за:

  • хранение состояния
  • обновление состояния
  • логику выбора
  • управление коллекциями

Пример:

useListState
useComboBoxState
useTreeState

Схема разделения

Архитектурно взаимодействие выглядит так:

UI слой (JSX + стили)
        ↓
React Aria (поведение)
        ↓
React Stately (состояние)

Каждый слой решает свою отдельную задачу.


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

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

Пример типичного компонента:

<Button variant="primary">Submit</Button>

Внутри такого компонента скрыто:

  • визуальное оформление
  • логика клика
  • фокус-менеджмент
  • ARIA-атрибуты
  • обработка клавиатуры

Этот подход имеет ряд недостатков:

Ограниченная кастомизация

Изменить структуру DOM невозможно.

Сложность переиспользования логики

Поведенческую часть нельзя применить в другом UI.

Зависимость от CSS библиотеки

Стили жестко связаны с компонентами.


Подход React Aria

React Aria предлагает headless-архитектуру.

Headless означает, что библиотека предоставляет:

  • поведение
  • доступность
  • обработку взаимодействий

но не навязывает разметку и стили.


Пример разделения ответственности

Рассмотрим кнопку.

Поведение

Хук:

useButton

Он реализует:

  • обработку кликов
  • поддержку клавиатуры
  • доступность
  • правильные роли ARIA

Представление

Разметка остается полностью под контролем разработчика.

Пример:

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
  • фокус клавиатурой
  • правильные ARIA-атрибуты
  • корректное поведение при disabled

Вместо того чтобы реализовывать это вручную, используется готовый хук.


Разделение логики взаимодействия

UI-компоненты обычно обрабатывают множество типов взаимодействия:

  • мышь
  • клавиатура
  • сенсорные устройства
  • screen readers

React Aria выделяет эту логику в отдельные утилиты:

usePress
useHover
useFocus
useKeyboard

Каждая из них отвечает за конкретный тип взаимодействия.


Пример: usePress

Этот хук объединяет разные способы активации элемента:

  • mouse click
  • touch
  • keyboard
  • pointer events

Пример:

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

Хуки лишь добавляют необходимые:

  • роли
  • ARIA-атрибуты
  • обработчики событий

Разделение состояния и интерфейса

Большинство сложных компонентов имеют внутреннее состояние.

Примеры:

  • выбранный элемент
  • открытое меню
  • введённый текст
  • фокус

React Aria не хранит состояние — оно вынесено в React Stately.


Пример: ListBox

Компонент списка использует два слоя.

Состояние

let state = useListState(props);

Состояние содержит:

  • элементы коллекции
  • выбранные элементы
  • активный элемент

Поведение

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

Этот хук добавляет:

  • keyboard navigation
  • ARIA роли
  • обработку фокуса

Представление

<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

Это делает систему крайне гибкой.


Переиспользуемые паттерны доступности

Сложные интерфейсные паттерны содержат десятки правил взаимодействия.

Примеры:

  • меню
  • комбобоксы
  • деревья
  • таблицы
  • автокомплит

Каждый из этих компонентов требует:

  • сложной клавиатурной навигации
  • фокус-менеджмента
  • ролей ARIA
  • взаимодействия со screen readers

React Aria выносит эти правила в отдельные хуки.


Пример сложного паттерна: ComboBox

ComboBox сочетает в себе:

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

В традиционной реализации всё это находится внутри одного компонента.

В React Aria логика распределяется.


Состояние

useComboBoxState

Отвечает за:

  • выбранный элемент
  • открытие списка
  • ввод текста

Поведение

useComboBox

Реализует:

  • ARIA-атрибуты
  • keyboard navigation
  • связь input и списка

UI

Полностью свободная JSX-разметка.


Улучшение тестируемости

Разделение ответственности значительно упрощает тестирование.

Можно тестировать:

состояние отдельно

useListState

поведение отдельно

usePress
useButton

визуальный слой отдельно

через snapshot-тесты или визуальные тесты.


Изоляция сложных алгоритмов

Некоторые аспекты UI-поведения крайне сложны:

  • управление фокусом
  • обработка pointer events
  • взаимодействие клавиатуры и мыши
  • поддержка accessibility

React Aria изолирует эти алгоритмы внутри хуков.

Разработчику не нужно реализовывать их вручную.


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

В больших приложениях число компонентов быстро растет.

Без разделения ответственности возникают проблемы:

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

React Aria позволяет строить иерархию абстракций:

Primitive hooks
      ↓
Headless components
      ↓
Design system components
      ↓
Application UI

Разделение на уровне дизайн-системы

React Aria идеально подходит для создания дизайн-систем.

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

1. Поведенческий слой

React Aria hooks.

2. Базовые headless компоненты

Например:

ButtonBase
ListBoxBase
MenuBase

3. Компоненты дизайн-системы

PrimaryButton
IconButton
DropdownMenu

Переиспользование между платформами

Разделение ответственности позволяет использовать одну и ту же логику:

  • в разных дизайн-системах
  • в разных проектах
  • в разных стилевых темах

Поскольку React Aria не содержит CSS, визуальный слой может полностью меняться.


Снижение сложности компонентов

Без разделения ответственности компонент может выглядеть так:

500–1000 строк кода

Внутри:

  • состояние
  • логика
  • DOM
  • стили
  • accessibility

После разделения:

UI компонент — 50–100 строк

Большая часть логики уже инкапсулирована в хуках.


Предсказуемость поведения

Когда поведение вынесено в отдельные хуки:

  • компоненты ведут себя одинаково
  • уменьшается вероятность ошибок
  • улучшается доступность интерфейса

Основные принципы разделения в React Aria

1. Headless архитектура

Библиотека не предоставляет стилизованные компоненты.


2. Поведение отделено от UI

Хуки отвечают за взаимодействие.


3. Состояние отделено от поведения

React Stately управляет состоянием.


4. Компоненты строятся через композицию

Мелкие хуки объединяются в более сложные.


5. UI полностью контролируется разработчиком

Разметка и стили не ограничены библиотекой.


Итоговая архитектурная модель

Application UI
      ↓
Design System Components
      ↓
Headless Components
      ↓
React Aria (behavior)
      ↓
React Stately (state)
      ↓
DOM

Такая модель делает интерфейсные компоненты:

  • гибкими
  • доступными
  • переиспользуемыми
  • масштабируемыми
  • независимыми от конкретной реализации UI.