Сохраняйте хуки без головы: разделение поведения уведомлений и их отображения
Узнайте, почему хук React, возвращающий JSX, скрывает часть дерева компонентов и связывает поведение с представлением, а также как переписать его так, чтобы он отображал состояние.
Источники: Why I’m Suspicious of Hooks That Return 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 внутри хука обычно означает объединение двух функций в одну абстракцию.
- Возвращаемое значение, похожее на данные, но на самом деле представляющее собой интерфейс, скрывает отчитчиков часть дерева компонентов.
- Сохраняйте хуки без головы: возвращайте состояние и действия, а отрисовку выполняйте в компонентах.
- Для общего отображения лучше извлекать отдельный компонент, чем возвращать маркипад из хука.
- При анализе такого хука задайте себе один вопрос: почему этот хук должен решать, как будет выглядеть интерфейс?