Главная / Статьи

Сохраняйте хуки без головы: разделение поведения уведомлений и их отображения

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

Издатель: REACTAPP.TOP5 мин чтения1092 слов

Источники: 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 внутри хука обычно означает объединение двух функций в одну абстракцию.
  • Возвращаемое значение, похожее на данные, но на самом деле представляющее собой интерфейс, скрывает отчитчиков часть дерева компонентов.
  • Сохраняйте хуки без головы: возвращайте состояние и действия, а отрисовку выполняйте в компонентах.
  • Для общего отображения лучше извлекать отдельный компонент, чем возвращать маркипад из хука.
  • При анализе такого хука задайте себе один вопрос: почему этот хук должен решать, как будет выглядеть интерфейс?