React паттерн headless component

Рассмотрим использование паттерна headless component для создания модульных и гибких компонентов в React приложениях.

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

Как это работает?

Суть этого шаблона заключается в том, чтобы отделить бизнес-логику от UI части самого компонента. Когда в будущем потребуется обновить пользовательский интерфейс, это можно сделать, не затрагивая код бизнес-логики. Паттерн, который мы собираемся использовать, называется headless component. В этом шаблоне компонент реализован в виде хуков React. Этот компонент отвечает за логику и управление состоянием. Headless-компонент ничего не знает о пользовательском интерфейсе. Создается отдельный компонент, который отвечает только за пользовательский интерфейс. Паттерн используется многими популярными библиотеками компонентов React, такими как: DownShift, React Table и т.д.

Ваш первый headless компонент

Начнем с написания первого React компонента с использованием этого шаблона. Ниже приведен пример компонента dropdown:
const useDropdown = (items: Item[]) => {
  // состояние
  const [selectedIndex, setSelectedIndex] = useState<number>(-1);
  const [isOpen, setIsOpen] = useState(false);
  const [selectedItem, setSelectedItem] = useState<Item | null>(null);

  // функция возвращающая aria-атрибуты для UI
  const getAriaAttributes = () => ({
    role: "combobox",
    "aria-expanded": isOpen,
    "aria-activedescendant": selectedItem ? selectedItem.text : undefined,
  });

  const handleKeyDown = (e: React.KeyboardEvent) => {
    // реализация на основе switch-case
  };
  
  const toggleDropdown = () => setIsOpen((isOpen) => !isOpen);

  return {
    isOpen,
    toggleDropdown,
    handleKeyDown,
    selectedItem,
    setSelectedItem,
    selectedIndex,
  };
};
Давайте интегрируем наш headless-компонент с компонентом, отвечающим за обработку пользовательского интерфейса:
const Dropdown = ({ items }: DropdownProps) => {
  const {
    isOpen,
    selectedItem,
    selectedIndex,
    toggleDropdown,
    handleKeyDown,
    setSelectedItem,
  } = useDropdown(items);

  return (
    <div className="dropdown" onKeyDown={handleKeyDown}>
      <Trigger
        onClick={toggleDropdown}
        label={selectedItem ? selectedItem.text : "Select an item..."}
      />
      {isOpen && (
        <DropdownMenu
          items={items}
          onItemClick={setSelectedItem}
          selectedIndex={selectedIndex}
        />
      )}
    </div>
  );
};
Выше приведен очень простой пример компонента. Представьте себе возможность, когда один и тот же шаблон интегрируется с более сложными приложениями. Шаблон headless component является чрезвычайно мощным, когда речь идет о создании перспективных компонентов с высокой степенью расширяемости.

Когда использовать этот шаблон?

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