Разделение логики и представления

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


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

React Aria предоставляет хуки, которые инкапсулируют всю поведенческую логику. Примеры таких хуков:

  • useButton — управление кнопками, включая обработку клавиатуры и доступных атрибутов (aria-pressed, disabled и т.д.).
  • useCheckbox — логика для чекбоксов с поддержкой indeterminate состояния.
  • useDialog — управление открытием и закрытием диалоговых окон с правильным фокусом.
  • useMenu и useMenuItem — поведение выпадающих меню и элементов меню.

Эти хуки не задают разметку и не накладывают стили. Они возвращают набор props и состояний, которые затем можно передать любому JSX-элементу.

Пример использования useButton:

import { useButton } from '@react-aria/button';
import { useRef } from 'react';

function AccessibleButton(props) {
  let ref = useRef();
  let { buttonProps } = useButton(props, ref);

  return <button {...buttonProps} ref={ref}>{props.children}</button>;
}

Здесь весь механизм обработки событий и доступности вынесен в useButton, а компонент отвечает только за визуальное представление.


Управление состоянием через React Stately

React Aria не занимается управлением состояния компонентов напрямую. Для этого используется библиотека React Stately, которая предоставляет хуки для хранения состояния, синхронизации с UI и управления сложными сценариями:

  • useCheckboxState — состояние для чекбоксов.
  • useRadioGroupState — логика для радиогрупп.
  • useMenuTriggerState — управление открытием и закрытием меню.
  • useListState — состояние списков и их элементов.

Пример комбинации React Aria и React Stately:

import { useCheckbox } from '@react-aria/checkbox';
import { useCheckboxState } from '@react-stately/checkbox';
import { useRef } from 'react';

function Checkbox({ children }) {
  let state = useCheckboxState();
  let ref = useRef();
  let { inputProps } = useCheckbox({ children }, state, ref);

  return (
    <label>
      <input {...inputProps} ref={ref} />
      {children}
    </label>
  );
}

Здесь useCheckboxState управляет состоянием чекбокса, а useCheckbox предоставляет доступные props для элемента <input>.


Вынесение визуальной части в отдельные компоненты

Разделение логики и представления подразумевает, что визуальная часть компонента не зависит от состояния напрямую. Компоненты представления получают состояние и props от логических хуков и рендерят их соответствующим образом.

Пример:

function ButtonView({ children, buttonProps }) {
  return <button {...buttonProps}>{children}</button>;
}

function Button({ children }) {
  let ref = useRef();
  let { buttonProps } = useButton({ onPress: () => alert('Clicked') }, ref);

  return <ButtonView buttonProps={buttonProps}>{children}</ButtonView>;
}

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


Работа с коллекциями и фокусом

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

  • корректное перемещение фокуса клавишами;
  • управление выбранными элементами;
  • поддержку aria-атрибутов для screen reader’ов.

Пример с меню:

import { useMenu, useMenuItem } from '@react-aria/menu';
import { useMenuTriggerState } from '@react-stately/menu';
import { useRef } from 'react';

function MenuButton() {
  let state = useMenuTriggerState({});
  let ref = useRef();
  let { menuTriggerProps, menuProps } = useMenuTrigger({}, state, ref);

  return (
    <>
      <button {...menuTriggerProps} ref={ref}>Open Menu</button>
      {state.isOpen && <Menu {...menuProps} />}
    </>
  );
}

function Menu({ menuProps }) {
  let ref = useRef();
  let { menuProps: ariaMenuProps } = useMenu({}, { items: ['Item 1', 'Item 2'] }, ref);

  return (
    <ul {...ariaMenuProps} ref={ref}>
      <MenuItem>Item 1</MenuItem>
      <MenuItem>Item 2</MenuItem>
    </ul>
  );
}

function MenuItem({ children }) {
  let ref = useRef();
  let { menuItemProps } = useMenuItem({ key: children }, {}, ref);

  return <li {...menuItemProps} ref={ref}>{children}</li>;
}

Здесь фокус и поведение меню полностью отделены от разметки, что обеспечивает гибкость и соответствие стандартам доступности.


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

  1. Повторное использование — логические хуки можно применять к любым визуальным компонентам.
  2. Тестируемость — логику можно тестировать независимо от UI.
  3. Гибкость стилизации — компоненты представления можно менять без влияния на поведение.
  4. Соответствие стандартам доступности — все aria-атрибуты и обработка событий инкапсулированы в хуках React Aria.

Стратегии для эффективного разделения

  • Логические хуки + компонент представления: всегда хранить состояние и обработку событий в хуках, а разметку и стили — в отдельном компоненте.
  • Передача props вниз: все props, возвращаемые хуками (buttonProps, inputProps, menuProps), передавать в компонент представления.
  • Минимизация побочных эффектов в UI: компонент визуализации не должен вызывать побочные эффекты, связанные с фокусом или клавиатурой.

Разделение логики и представления в React Aria создаёт устойчивую архитектуру компонентов, где изменения в UI не влияют на поведение, а любые улучшения доступности реализуются централизованно в хуках. Это подход, который формирует основу для создания масштабируемых, доступных и легко поддерживаемых интерфейсов.