Головна / Статті

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

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

Видавець: REACTAPP.TOP5 хв читання1092 слів

Джерела: React 27: Higher Order Components (HOC) — Deep Dive (Class vs Functional Components)

Чому 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 під час відображення.