Синхронизация с состоянием

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

Контролируемые и неконтролируемые компоненты

Контролируемый компонент получает значение и функции обновления из внешнего состояния. Например, для поля ввода:

import { useTextField } from "@react-aria/textfield";
import { useState } from "react";

function ControlledInput() {
  const [value, setValue] = useState("");
  const { inputProps } = useTextField({
    label: "Имя",
    value,
    onChange: setValue
  });
  return <input {...inputProps} />;
}

Ключевые моменты:

  • value полностью управляется внешним состоянием.
  • onChange синхронизирует Aria с React состоянием.
  • Любые изменения состояния автоматически обновляют UI и доступность компонента.

Неконтролируемый компонент хранит своё внутреннее состояние. Он удобен, когда внешний контроль не нужен:

import { useTextField } from "@react-aria/textfield";

function UncontrolledInput() {
  const { inputProps } = useTextField({
    label: "Имя",
    defaultValue: "Иван"
  });
  return <input {...inputProps} />;
}

Особенности:

  • defaultValue задаёт начальное состояние.
  • В дальнейшем состояние управляется внутренне компонентом.
  • Aria автоматически обновляет доступность при изменении значения.

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

Для сложных компонентов, таких как Select, ComboBox, CheckboxGroup, React Aria использует хуки состояния из react-stately. Например:

import { useComboBoxState } from "@react-stately/combobox";
import { useComboBox } from "@react-aria/combobox";

function FruitSelector({ fruits }) {
  const state = useComboBoxState({ items: fruits });
  const { inputProps, listBoxProps, labelProps } = useComboBox({}, state);

  return (
    <div>
      <label {...labelProps}>Выберите фрукт</label>
      <input {...inputProps} />
      {state.isOpen && (
        <ul {...listBoxProps}>
          {state.collection.map(item => (
            <li key={item.key}>{item.rendered}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Здесь:

  • useComboBoxState управляет внутренним состоянием выбора.
  • useComboBox обеспечивает синхронизацию ARIA атрибутов с этим состоянием.
  • state.isOpen управляет видимостью выпадающего списка.
  • state.collection обеспечивает актуальные элементы для UI и доступности.

Управление состоянием фокуса и выделения

React Aria разделяет визуальное состояние и логическое состояние для доступности. Хуки useFocusRing, useHover и useSelection позволяют синхронизировать UI и ARIA атрибуты.

import { useFocusRing } from "@react-aria/focus";
import { useState } from "react";

function FocusableButton() {
  const [isPressed, setPressed] = useState(false);
  const { focusProps, isFocusVisible } = useFocusRing();

  return (
    <button
      {...focusProps}
      onMouseD own={() => setPressed(true)}
      onMous eUp={() => setPressed(false)}
      style={{
        outline: isFocusVisible ? "2px solid blue" : "none",
        background: isPressed ? "lightgray" : "white"
      }}
    >
      Нажми меня
    </button>
  );
}

Особенности:

  • isFocusVisible синхронизирует визуальную подсветку фокуса с состоянием.
  • Локальное состояние isPressed отражает состояние нажатия.
  • ARIA атрибуты (aria-pressed, aria-selected) могут быть привязаны к этим состояниям для поддержки вспомогательных технологий.

Поддержка асинхронного состояния

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

import { useComboBoxState } from "@react-stately/combobox";
import { useComboBox } from "@react-aria/combobox";
import { useEffect, useState } from "react";

function AsyncFruitSelector() {
  const [fruits, setFruits] = useState([]);
  const state = useComboBoxState({ items: fruits });
  const { inputProps, listBoxProps, labelProps } = useComboBox({}, state);

  useEffect(() => {
    fetch("/api/fruits")
      .then(res => res.json())
      .then(data => setFruits(data));
  }, []);

  return (
    <div>
      <label {...labelProps}>Выберите фрукт</label>
      <input {...inputProps} />
      {state.isOpen && (
        <ul {...listBoxProps}>
          {state.collection.map(item => (
            <li key={item.key}>{item.rendered}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
  • Изменение состояния fruits автоматически обновляет state.collection.
  • UI и ARIA атрибуты остаются синхронизированы.
  • Это обеспечивает корректное взаимодействие с экранными читалками и клавиатурной навигацией.

Рекомендации по архитектуре состояния

  1. Разделение состояния и представления: держать логику состояния отдельно в react-stately, а отображение — в компонентах с react-aria.
  2. Использовать контролируемое состояние для сложных компонентов, где важна внешняя синхронизация (формы, модальные окна).
  3. Применять useFocusRing и useHover для точной синхронизации визуальных и ARIA состояний.
  4. Асинхронные данные всегда обновлять через состояние react-stately, чтобы компоненты оставались доступными и корректными при динамическом рендеринге.

Итоговый подход

Синхронизация состояния в React Aria строится вокруг идеи: любое изменение данных должно отражаться одновременно в UI и ARIA атрибутах. Хуки Aria обеспечивают корректное поведение компонентов для пользователей с ограниченными возможностями, а react-stately управляет внутренней логикой состояния, включая выбор, открытие списков, фокус, выделение и асинхронные обновления. Такой подход позволяет создавать доступные, реактивные и легко масштабируемые интерфейсы.