Разделение пользовательского интерфейса и логики в React: чистый код с безголовыми компонентами

В сфере фронтенд-разработки встречаются сложные термины и парадигмы. “Безголовый пользовательский интерфейс” (“headless UI”) или “безголовые компоненты” (“headless components”) вполне можно отнести к этой категории. Не одни вы ломаете голову, пытаясь понять, что они означают. На самом деле, несмотря на названия, эти понятия представляют собой впечатляющие стратегии, способные значительно упростить управление сложными пользовательскими интерфейсами.

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

Для наглядности начнем с рассмотрения простого, но эффективного применения безголовых компонентов: извлечения хука “useToggle” из двух похожих компонентов для уменьшения дублирования кода. Хотя этот пример может показаться тривиальным, он облегчает понимание основных принципов работы безголовых компонентов. Выявление общих закономерностей и выделение их в многократно используемые части позволит упорядочить кодовую базу и проложить путь к более эффективному процессу разработки.

Но это только вершина айсберга! По мере углубления мы столкнемся с более сложным примером применения этого принципа: использованием Downshift  —  мощной библиотеки для создания продвинутых компонентов ввода.

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

Компонент Toggle

Компоненты Toggle (тумблеры) являются неотъемлемой частью многочисленных приложений. Они являются “молчаливыми исполнителями” таких функций, как “запомнить меня на этом устройстве” (“remember me on this device”), “активировать уведомления” (“activate notifications”), а также популярного “темного режима” (“dark mode”).

Компонент ToggleButton

Создать такой тумблер в React легко. Рассмотрим его.

const ToggleButton = () => {
const [isToggled, setIsToggled] = useState(false);

const toggle = useCallback(() => {
setIsToggled((prevState) => !prevState);
}, []);

return (
<div className="toggleContainer">
<p>Do not disturb</p>
<button onClick={toggle} className={isToggled ? "on" : "off"}>
{isToggled ? "ON" : "OFF"}
</button>
</div>
);
};

Хук useState устанавливает переменную состояния isToggled с начальным значением false. Функция toggle, созданная с помощью useCallback, при каждом вызове (при нажатии на кнопку) переключает значение isToggled между true и false. Внешний вид кнопки и ее текст (“ON” и “OFF”) динамически отражают состояние isToggled.

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

Компонент ExpandableSection

Реализация тоже не слишком сложна  —  можно просто сделать вот так:

const ExpandableSection = ({ title, children }: ExpandableSectionType) => {
const [isOpen, setIsOpen] = useState(false);

const toggleOpen = useCallback(() => {
setIsOpen((prevState) => !prevState);
}, []);

return (
<div>
<h2 onClick={toggleOpen}>{title}</h2>
{isOpen && <div>{children}</div>}
</div>
);
};

Здесь прослеживается явное сходство  —  состояния “‘on” (“включено”) и “off” (“выключено”) в ToggleButton похожи на действия “expand” (“развернуть”) и “collapse” (“свернуть”) в ExpandableSection. Понимание такой схожести позволяет абстрагировать общую функциональность в отдельную функцию. В экосистеме React это можно сделать с помощью создания пользовательского хука.

const useToggle = (init = false) => {
const [state, setState] = useState(init);

const toggle = useCallback(() => {
setState((prevState) => !prevState);
}, []);

return [state, toggle];
};

Эта реорганизация кода может показаться довольно простой, но она подчеркивает важную концепцию: разделение поведения и представления. В данном сценарии пользовательский хук выступает в качестве машины состояний, не зависящей от JSX. И ToggleButton, и ExpandableSection используют эту же базовую логику.

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

Безголовый компонент

Существует множество замечательных библиотек, использующих данный паттерн для разделения поведения (или управления состоянием) и представления. Среди этих компонентных библиотек наиболее известной является Downshift.

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

Допустим, я хочу создать выпадающий список. Очевидно, что мне потребуются данные списка, триггер и несколько настроек для выделения выбранного элемента, количества отображаемых строк. Но я не хочу создавать всю доступность с нуля, поскольку необходимо учесть множество граничных случаев, включая кроссбраузерность и нюансы работы на различных устройствах.

Компонент StateSelect

Таким образом, с помощью нескольких строк JSX можно легко создать полностью доступный select, используя Downshift:

const StateSelect = () => {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({items: states});

return (
<div>
<label {...getLabelProps()}>Issued State:</label>
<div {...getToggleButtonProps()} className="trigger" >
{selectedItem ?? 'Select a state'}
</div>
<ul {...getMenuProps()} className="menu">
{isOpen &&
states.map((item, index) => (
<li
style={
highlightedIndex === index ? {backgroundColor: '#bde4ff'} : {}
}
key={`${item}${index}`}
{...getItemProps({item, index})}
>
{item}
</li>
))}
</ul>
</div>
)
}

Этот компонент представляет собой селектор состояний, использующий хук useSelect в Downshift. Он позволяет пользователям выбирать состояние из выпадающего меню.

  • useSelect управляет состоянием и взаимодействиями при вводе select. 
  • isOpen, selectedItem и highlightedIndex  —  это переменные состояния, управляемые useSelect.
  • getToggleButtonProps, getLabelProps, getMenuProps и getItemProps  —  это функции для предоставления необходимых пропсов соответствующим элементам.
  • isOpen определяет, открыт ли выпадающий список.
  • selectedItem содержит значение текущего выбранного состояния.
  • highlightedIndex указывает, какой элемент списка выделен в данный момент.
  • Если выпадающий список открыт, то states.map формирует неупорядоченный список состояний, из которых можно выбирать.
  • Оператор spread (...) используется для передачи пропсов из хуков Downshift в компоненты. Сюда входят такие элементы, как обработчики нажатий, навигация по клавиатуре и свойства ARIA.
  • Если состояние выбрано, то оно отображается в виде содержимого кнопки. В противном случае отображается “Select a state” (“Выбрать состояние”).

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

Существует еще несколько библиотек безголовых компонентов, которые уже следуют этому паттерну.

  • Reakit. Предоставляет набор безголовых компонентов для создания доступных высокоуровневых библиотек пользовательского интерфейса, наборов инструментов, систем проектирования и т. д.
  • React Table. Это безголовая утилита, предназначенная для композиций. Она основана на хуках и позволяет создавать всевозможные таблицы.
  • react-use. Это набор хуков, включающий несколько безголовых компонентов.

Погрузимся немного глубже

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

Паттерн Headless UI

При такой схеме Headless UI (безголового пользовательского интерфейса) на самом верхнем уровне определяется JSX (или большинство тегов), который отвечает только за отображение переданных свойств. Сразу под ним находится то, что называется “безголовым компонентом”. Этот компонент поддерживает все поведения, управляет состояниями и предоставляет интерфейс для взаимодействия с JSX. В основе этой структуры лежат модели данных, инкапсулирующие логику, специфичную для конкретной области. Эти модели не имеют отношения к пользовательскому интерфейсу или состояниям. Они сосредоточены на управлении данными и бизнес-логике. Такой многоуровневый подход обеспечивает четкое разделение задач, повышая ясность и удобство работы с кодом.

Необходимость взвешенного подхода

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

  • Возможность повторного применения. Основным преимуществом безголовых компонентов является возможность их повторного использования. Инкапсулируя логику в отдельные компоненты, можно повторно использовать эти компоненты в нескольких элементах пользовательского интерфейса. Это не только уменьшает дублирование кода, но и способствует согласованности всего приложения.
  • Разделение задач. Безголовые компоненты четко разделяют логику и представление. Это делает кодовую базу более управляемой и понятной, особенно для больших команд с разделенными обязанностями.
  • Гибкость. Поскольку безголовые компоненты не диктуют представления, они обеспечивают большую гибкость при проектировании. Вы можете изменять пользовательский интерфейс по своему усмотрению, не затрагивая при этом базовую логику.
  • Тестируемость. Поскольку логика отделена от представления, писать модульные тесты для бизнес-логики проще.

С другой стороны, этот паттерн, как правило, немного сложнее, чем компонент типа “все в одном”. Поэтому при его использовании, учитывайте следующие соображения.

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

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

Заключение

В этой статье мы погрузились в мир безголовых пользовательских интерфейсов (Headless User Interfaces), представляющих собой эффективный подход к решению сложных задач пользовательского интерфейса. Мы рассмотрели, как отделение поведения от рендеринга позволяет создавать более удобный для сопровождения и многократного использования код, сокращая избыточность и потенциальные ошибки.

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Juntao Qiu: Decoupling UI and Logic in React: A Clean Code Approach with Headless Components

Предыдущая статья7 лучших CLI-библиотек Python в 2023 году
Следующая статьяЧто такое ViewModel