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

Компоненты высшего порядка: от обёрток классов до функций и хуков

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

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

Источники: React 27: Higher Order Components (HOC) — Deep Dive (Class vs Functional Components)

Задолго до появления пользовательских хуков разработчикам React требовался способ обмена поведением, таким как логирование, проверка авторизации или загрузка данных, между множеством компонентов. Решением стали компоненты высшего порядка (HOC): обычные функции, которые принимают компонент и создают вокруг него усиленный обертку. Ниже показано, как тот же HOC для логирования реализуется в виде класса и функции, после чего его сравнивают с пользовательским хуком.

Почему появились HOC в эпоху классов

Когда большинство кода на React писалось в виде классов, повторное использование логики было затруднительным. Представьте, что компоненты Profile, Dashboard и Settings должны все записывать логи момента своего загрузки и обновления, проверять наличие авторизованного пользователя, загружать данные и обрабатывать ошибки. Без общей абстракции каждый класс повторял один и тот же конструктор, настройку состояния, методы жизненного цикла и вспомогательные методы. HOC решали эту проблему, оборачивая класс в другой класс, который содержал общую логику жизненного цикла.

HOC — это не специальный API React, а просто композиция функций, применяемая к компонентам.

HOC для логирования, созданный с использованием классов

Написание обёртки

Следующий HOC возвращает новый класс, который записывает логи момента загрузки и обновления обёрнутого компонента, а затем отображает исходный компонент с передачей всех поступающих атрибутов:

import React, { Component } from "react";

export function withRenderLogger(WrappedComponent) {
  return class extends Component {
    componentDidMount() {
      console.log(`${WrappedComponent.name} mounted`);
    }
    componentDidUpdate() {
      console.log(`${WrappedComponent.name} updated`);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

Обертка управляет методами жизненного цикла и передаёт this.props без изменений, поэтому в исходном компоненте отсутствует код для логирования.

Применение

Вместо сырой классовой структуры экспортируется версия, обёрнутая в эту обертку:

import React, { Component } from "react";
import { withRenderLogger } from "./withRenderLogger";

class Profile extends Component {
  render() {
    return <h2>Profile Component</h2>;
  }
}
export default withRenderLogger(Profile);

При отрисовке Profile теперь сначала отрисовывается обертка, которая затем отрисовывает настоящий Profile. Возвращаемый класс является анонимным, поэтому установка такого параметра, как displayName, например withRenderLogger(Profile), облегчает чтение в React DevTools.

Потери, связанные с HOC на основе классов

Этот подход сработал, но с определёнными трудностями:

  • вложенные определения классов и привязка к this
  • логика, распределённая по методам жизненного цикла
  • более сложные деревья компонентов, из-за чего отладка требует просмотра всех обёрток
  • коллизии имен свойств, когда несколько HOC вставляют свойства с одинаковыми именами
  • косвенная связь между HOC и параметрами компонента, который он оборачивает
  • Та же идея с функциональными компонентами

    Без классов, this и методов жизненного цикла HOC сводится к функции, возвращающей функциональный компонент:

    function HOC(WrappedComponent) {
      return function Enhanced(props) {
        return <WrappedComponent {...props} />;
      };
    }
    

    Вот логгер, переписанный в этом стиле:

    export function withLogger(WrappedComponent) {
      return function Enhanced(props) {
        console.log(`${WrappedComponent.name} rendered`);
       return <WrappedComponent {...props} />;
      };
    }
    

    Способ использования идентичен версии с классами:

    import React from "react";
    import { withLogger } from "./withLogger";
    
    function Dashboard() {
      return <h2>Dashboard Component</h2>;
    }
    export default withLogger(Dashboard);
    

    Он ведет лог при каждой отрисовке, но как побочный эффект внутри тела отрисовки. React может отрисовывать компоненты чаще, чем фактически сохраняет изменения, а в режиме Strict Mode отрисовка происходит дважды во время разработки, поэтому ожидайте дублирующихся записей в логе; для чего-то более сложного, чем простая отладка, используйте useEffect.

    Небольшое демо-приложение

    Кнопка переключает между классовым Profile и функциональным Dashboard, так что оба HOC ведут логирование. Использование отрисовки вместо выбрасываемых ошибок делает демонстрацию предсказуемой при режиме Fast Refresh:

    import React, { useState } from "react";
    import Profile from "./Profile";
    import Dashboard from "./Dashboard";
    
    export default function App() {
      const [toggle, setToggle] = useState(true);
      return (
        <div style={{ padding: 20 }}>
          <h1>HOC Demo</h1>
          <button onClick={() => setToggle(!toggle)}>
            Re-render Components
          </button>
          {toggle ? <Profile /> : <Dashboard />}
        </div>
      );
    }
    

    Profile.js использует классовый HOC:

    import React, { Component } from "react";
    import { withRenderLogger } from "./withRenderLogger";
    
    class Profile extends Component {
      render() {
        return <h2>Profile Component</h2>;
      }
    }
    export default withRenderLogger(Profile);
    

    Dashboard.js использует функциональный HOC:

    import React from "react";
    import { withLogger } from "./withLogger";
    
    function Dashboard() {
      return <h2>Dashboard Component</h2>;
    }
    export default withLogger(Dashboard);
    

    Модуль классового HOC:

    import React, { Component } from "react";
    
    export function withRenderLogger(WrappedComponent) {
      return class extends Component {
        componentDidMount() {
          console.log(`${WrappedComponent.name} mounted`);
        }
        componentDidUpdate() {
          console.log(`${WrappedComponent.name} updated`);
        }
        render() {
          return <WrappedComponent {...this.props} />;
        }
      };
    }
    

    А модуль функционального HOC:

    export function withLogger(WrappedComponent) {
      return function Enhanced(props) {
        console.log(`${WrappedComponent.name} rendered`);
        return <WrappedComponent {...props} />;
      };
    }
    

    Эти файлы работают в любой стандартной среде React; при использовании Vite файлы JSX называются .jsx.

    Почему пользовательские хуки заменили большинство HOC

    Пользовательские хуки теперь покрывают большинство прежних сценариев использования HOC: загрузка удаленных данных, проверки авторизации, отслеживание, обработчики DOM и подписки. Механизм логирования теперь представлен в виде хука:

    function useLogger(name) {
      useEffect(() => {
        console.log(`${name} rendered`);
      });
    }
    
    function Dashboard() {
      useLogger("Dashboard");
      return <h2>Dashboard Component</h2>;
    }
    

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

    HOC всё ещё подходят для условной обертки (показа экрана входа вместо самого компонента), библиотек с API HOC и кода на основе классов; React.memo имеет аналогичную структуру. Применяйте HOC на уровне модуля: создание обертки во время отрисовки приводит к переустановке поддерева и потере его состояния.

    Основные выводы

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