Зберігайте гачки без голівки: розділення поведінки сповіщень від їх відображення
Дізнайтеся, чому хук React, який повертає JSX, приховує частину дерева компонентів та поєднує поведінку з візуалізацією, а також як переписати його так, щоб він відображав стан.
Кастомні хуки чудово підходять для приховування деталей реалізації, але іноді вони приховують занадто багато. Хук, який повертає готовий JSX, здається зручним у місці виклику, але він переносить частину вашої інтерфейсної структури за межі API, яке виглядає як звичайні дані, та прив’язує поведінку до певного формату відображення. Використовуючи типовий хук для сповіщень з редактора панелей керування, ця стаття показує, як розпізнати таку проблему, як переписати код на безінтерфейсний хук та яку користь можна отримати від додаткових кількох рядків коду.
Хук для сповіщень, який сам відображає інтерфейс
Сповіщення ідеально підходять для хуків: їм потрібен стан, можливо таймери, функції закриття та певна система чергування. Ось як використовується такий хук:
const {
showSuccess,
showError,
alerts,
} = useAlerts();
return (
<>
{alerts}
<Dashboard />
</>
);
Вже є ознаки того, що щось не так. Функція alerts безпосередньо додається до JSX, що сильно свідчить про те, що хук повертає вже оформлені елементи. Хук відповідає за поведінку, адже він надає функції showSuccess та showError, а також, схоже, впливає на зовнішній вигляд сповіщень.
Реалізація підтверджує це:
const useAlerts = () => {
const [alerts, setAlerts] = useState<Alert[]>([]);
const showSuccess = (message: string) => {
// update state
};
const showError = (message: string) => {
// update state
};
...
return {
showSuccess,
showError,
alerts: (
<AlertContainer>
{alerts.map(({ id, type, message }) => (
<Alert
key={id}
type={type}
message={message}
/>
))}
</AlertContainer>
),
};
};
useAlerts керує станом сповіщень, функціями, які його змінюють, та самим форматуванням: обгорткою AlertContainer та одним елементом Alert на кожен запис. Межа між логікою та презентацією зникла.
Чому повернений JSX вводить в оману
Він виглядає як дані
З боку користувача немає жодних ознак того, що alerts є піддеревом React. Він знаходиться поруч із двома функціями, має множинну назву, яка вказує на масив записів, і його можна легко сплутати зі станом:
const {
showSuccess,
showError,
alerts,
} = useAlerts();
Єдиний спосіб дізнатися правду — це відкрити цей хук.
Він приховує джерело UI
Дерево компонентів є корисним частково тому, що воно вказує, звідки походить кожна частина інтерфейсу. Коли переглядаєте панель керування та бачите це:
{alerts}
<Dashboard />
ви не можете зрозуміти, що відображає alerts, які компоненти він інсталює, від якого контексту чи атрибутів залежать ці компоненти, чи як вони оформлені, не заглянувши у хук. Частина дерева була переміщена за щось, що виглядає як поведінковий елемент.
Він поєднує два незалежні рішення
Хук більше не може відображати поведінку сповіщення без одночасного визначення компонента сповіщення. Будь-який екран, який потребує логіки сповіщення, успадковує його вигляд, незалежно від того, чи підходить він чи ні. Не існує технічних причин, чому ці два аспекти мають бути пов’язані.
Рефакторинг до безголового хука
Хук має контролювати стан та операції з ним, і ніщо більше. Необхідно відображати сирий список сповіщень разом із діями, включаючи спосіб скасування сповіщення:
const {
alerts,
showSuccess,
showError,
dismissAlert,
} = useAlerts();
Потім відображайте сповіщення у компоненті, де це вже належить до його функцій:
<AlertContainer>
{alerts.map(({ id, type, message }) => (
<Alert
key={id}
type={type}
message={message}
onDismiss={() => dismissAlert(id)}
/>
))}
</AlertContainer>
<Dashboard />
Тепер дерево компонентів знову є чітким. Кожен, хто його читає, бачить AlertContainer, по одному Alert на кожен запис, ідентифікований за ключем id, а також обробник для скасування, підключений до хука. Роль хука є однозначною: він забезпечує стан та дії.
Чи є додатковий код проблемою?
Компонент, який використовує дані, дійсно стає довшим, і це нормально. Існує спокуса оптимізувати API до такого рівня, щоб користувач майже нічого не писав, але така оптимізація може стерти корисну архітектурну інформацію. П’ять рядків коду з очевидною структурою легше підтримувати, ніж один {alerts}, який приховує цілу частину інтерфейсу.
Якщо кілька екранів відображають сповіщення однаково, ви все одно можете уникнути повторень, не повертаючи інтерфейс до хука: створіть компонент AlertList, який приймає як параметри alerts та onDismiss, або такий, який сам викликає хук. У будь-якому разі відображення знаходиться у компоненті, який видно в дереві та може піддаватися профілюванню, мемоїзації та тестуванню, як і будь-який інший компонент.
Можливість повторного використання
Оскільки JSX вбудований, повторне використання логіки змушує також повторно використовувати презентацію, що є припущенням, яке не завжди справедливе. У безголовій версії зона повідомлень, банер у тексті та мобільний формат можуть мати абсолютно однаковий цикл життя повідомлення, проте відображатися по-різному. Компонент повідомлення також може еволюціонувати самостійно, не впливаючи на хук, який керує таймінгом та скасуванням.
Пов’язана проблема: хуки, які повертають компоненти
Близьким аналогом цієї проблеми є хук, який визначає та повертає функцію компонента, наприклад const AlertView = () => ... у тілі хука. Оскільки при кожному відображенні створюється нова функція, React кожного разу бачить новий тип компонента та демонтуює та перевстановлює відповідну піддеревину, стираючи її внутрішній стан та DOM. Повернення елементів, як це робить початковий хук, уникає цієї конкретної проблеми, але вищеописана проблема з проектуванням залишається.
Основні висновки
- Використання JSX у механізмі hook зазвичай означає об’єднання двох функцій у одну абстракцію.
- Значення, яке повертається та виглядає як дані, але насправді є частиною інтерфейсу, приховує частину дерева компонентів від користувача.
- Зберігайте hook без власних елементів: повертайте стан та дії, а відображення реалізуйте у компонентах.
- Для спільного відображення даних краще виділити окремий компонент, ніж повертати маркап з hook.
- Під час аналізу такого hook поставте собі одне запитання: яка причина змушує цей hook вирішувати, як має виглядати інтерфейс?