Навигация

HOC: паттерн обёрток, композиция и сравнение с хуками

Higher-Order Component (HOC, компонент высшего порядка) — один из классических паттернов React для переиспользования логики между компонентами. HOC — это функция, которая принимает компонент и возвращает новый компонент с расширенным поведением. До появления хуков HOC был основным механизмом разделения логики (наряду с render props), и до сих пор широко используется в экосистеме — React.memo, React.forwardRef и многие библиотеки (Redux connect, React Router withRouter) построены на этом паттерне.


Содержание

  1. Что такое HOC и откуда он взялся
  2. Базовая механика
  3. Реальные примеры HOC
  4. Композиция HOC
  5. Проблемы и подводные камни
  6. HOC vs хуки
  7. HOC vs Render Props
  8. Vue-аналоги
  9. Когда HOC всё ещё актуален
  10. Ключевые тезисы для интервью
  11. Заключение

Что такое HOC и откуда он взялся

HOC — это паттерн, заимствованный из функционального программирования. Компонент высшего порядка — это функция:

hocComponent = (WrappedComponent) => EnhancedComponent

В раннем React (до хуков, до 16.8) не было способа вынести логику с состоянием из компонента. Классы предоставляли componentDidMount, setState, но переиспользовать эту логику между компонентами было сложно. HOC стал ответом — обёртка, которая добавляет поведение любому компоненту, не меняя его кода.

Vue-аналог: во Vue аналогичную роль играют миксины (legacy) и композаблы (Composition API). Композаблы — более современный и предпочтительный подход, функционально эквивалентный React-хукам. HOC во Vue встречается редко, но технически реализуем через defineComponent с обёрткой.


Базовая механика

HOC — обычная JavaScript-функция, возвращающая компонент. Внутри она рендерит обёрнутый компонент, добавляя свои пропсы или поведение:

function withLogging(WrappedComponent) {
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log(`Rendered ${WrappedComponent.name}`, props);
    });

    return <WrappedComponent {...props} />;
  };
}

// Использование
const MyComponentWithLogging = withLogging(MyComponent);

Ключевые принципы

  1. HOC не мутирует исходный компонент. Он создаёт новый, обёрнутый.
  2. HOC — чистая функция без побочных эффектов в самом факте создания (логика живёт внутри рендера или эффектов).
  3. Пропсы передаются через — HOC добавляет свои пропсы, но не удаляет существующие (контракт «передай всё, что получил»).
  4. Имя обёрнутого компонента полезно для DevTools — установите displayName:
function withAuth(WrappedComponent) {
  function EnhancedComponent(props) {
    // ...
    return <WrappedComponent {...props} />;
  }
  EnhancedComponent.displayName = `WithAuth(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
  return EnhancedComponent;
}

Реальные примеры HOC

1. Авторизация — защита маршрута

Классический сценарий: компонент рендерится только если пользователь авторизован, иначе — редирект:

function withAuth(WrappedComponent) {
  return function ProtectedRoute(props) {
    const user = useAuth();

    if (!user) {
      return <Navigate to="/login" replace />;
    }

    return <WrappedComponent {...props} user={user} />;
  };
}

// Использование
const Dashboard = withAuth(DashboardPage);
const Settings = withAuth(SettingsPage);

2. Загрузка данных

HOC, который загружает данные и передаёт их в обёрнутый компонент:

function withData(WrappedComponent, dataFetcher) {
  return function DataComponent(props) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
      let cancelled = false;
      setLoading(true);

      dataFetcher(props.id)
        .then(result => {
          if (!cancelled) {
            setData(result);
            setLoading(false);
          }
        })
        .catch(err => {
          if (!cancelled) {
            setError(err.message);
            setLoading(false);
          }
        });

      return () => { cancelled = true; };
    }, [props.id]);

    if (loading) return <Spinner />;
    if (error) return <ErrorMessage message={error} />;

    return <WrappedComponent {...props} data={data} />;
  };
}

// Использование
const UserProfileWithData = withData(UserProfile, fetchUserProfile);
const ProductDetailsWithData = withData(ProductDetails, fetchProduct);

3. Подписка на внешние источники

function withSubscription(WrappedComponent, subscribeFn) {
  return function SubscribedComponent(props) {
    const [data, setData] = useState(() => subscribeFn.getData());

    useEffect(() => {
      const handleChange = () => setData(subscribeFn.getData());
      subscribeFn.subscribe(handleChange);
      return () => subscribeFn.unsubscribe(handleChange);
    }, []);

    return <WrappedComponent {...props} data={data} />;
  };
}

4. React.memo — встроенный HOC

React.memo — самый распространённый HOC в любом React-приложении. Он принимает компонент и возвращает мемоизированную версию, пропускающую ререндер при неизменных пропсах:

const MemoizedButton = React.memo(function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
});

5. React.forwardRef — HOC для проброса ref

const FancyInput = React.forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />;
});

Композиция HOC

HOC легко комбинируются — один HOC оборачивает результат другого. Это позволяет собирать сложное поведение из простых кирпичиков:

// Компонент получает: авторизацию + данные + логирование
const EnhancedDashboard = withLogging(
  withData(
    withAuth(DashboardPage),
    fetchDashboardData
  )
);

Функция-композиция для читаемости

Вложенные вызовы читаются снизу вверх, что неудобно. Утилита compose (из Redux) инвертирует порядок:

const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)));

const enhance = compose(
  withAuth,
  withData(fetchDashboardData),
  withLogging
);

const EnhancedDashboard = enhance(DashboardPage);
// Читается слева направо: авторизация → данные → логирование

Порядок имеет значение

Порядок композиции влияет на поведение:

// withAuth снаружи: если нет пользователя, withData не вызовется
const A = withAuth(withData(Component, fetcher));

// withData снаружи: данные загружаются даже без авторизации
const B = withData(withAuth(Component), fetcher);

Проблемы и подводные камни

1. «Обёртка в обёртке» в DevTools

Без displayName дерево компонентов превращается в кашу из безымянных функций. Всегда устанавливайте имя:

EnhancedComponent.displayName = `WithData(${WrappedComponent.name})`;

2. Потеря статических методов

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

class Original extends React.Component {
  static fetchData = () => { /* ... */ };
  render() { return <div />; }
}

const Enhanced = withAuth(Original);
Enhanced.fetchData; // undefined!

Решение — библиотека hoist-non-react-statics, которая копирует нереактовские статические методы на обёртку:

import hoistNonReactStatics from "hoist-non-react-statics";

function withAuth(WrappedComponent) {
  function EnhancedComponent(props) { /* ... */ }
  hoistNonReactStatics(EnhancedComponent, WrappedComponent);
  return EnhancedComponent;
}

3. Конфликт пропсов

Если HOC добавляет пропс user, и обёрнутый компонент тоже ожидает user — возникает коллизия. Решение — именовать пропсы с префиксом или использовать соглашение:

// HOC добавляет authUser, а не user, чтобы избежать конфликта
return <WrappedComponent {...props} authUser={user} />;

4. Проблемы с типизацией в TypeScript

HOC усложняют вывод типов — обёрнутый компонент теряет часть типизации, и TypeScript не всегда корректно определяет пропсы. Хуки в этом плане значительно проще:

// С HOC — нужно вручную пробрасывать типы
function withAuth<P extends { user: User }>(
  WrappedComponent: React.ComponentType<P>
): React.ComponentType<Omit<P, "user"> & { optionalUser?: User }> {
  // ...
}

// С хуком — типы работают естественно
function useAuth(): User | null { /* ... */ }
function Dashboard({ user }: { user: User }) { /* ... */ }

5. HOC внутри рендера — антипаттерн

Никогда не создавайте HOC внутри рендера другого компонента — это приведёт к размонтированию и пересозданию дерева при каждом ререндере:

// ❌ Катастрофа: Enhanced создаётся заново при каждом рендере,
// что приводит к потере состояния и DOM
function Parent() {
  const Enhanced = useMemo(() => withAuth(MyComponent), []);
  return <Enhanced />;
}

// ✅ HOC создаётся один раз, на уровне модуля
const Enhanced = withAuth(MyComponent);
function Parent() {
  return <Enhanced />;
}

HOC vs хуки

С появлением хуков в React 16.8 (2019) HOC во многом утратил актуальность как способ переиспользования логики с состоянием. Сравним:

Один и тот же сценарий: подписка на window size

HOC:

function withWindowSize(WrappedComponent) {
  return function WindowSizeComponent(props) {
    const [size, setSize] = useState({
      width: window.innerWidth,
      height: window.innerHeight,
    });

    useEffect(() => {
      const handleResize = () =>
        setSize({ width: window.innerWidth, height: window.innerHeight });
      window.addEventListener("resize", handleResize);
      return () => window.removeEventListener("resize", handleResize);
    }, []);

    return <WrappedComponent {...props} windowSize={size} />;
  };
}

// Использование
const MyComponent = withWindowSize(OriginalComponent);

Хук:

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () =>
      setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return size;
}

// Использование
function MyComponent() {
  const size = useWindowSize();
  // ...
}

Сравнительная таблица

КритерийHOCХуки
Переиспользование логики с состояниемДаДа
Прозрачность пропсовНепрозрачная — добавляются «магические» пропсыПрозрачная — переменная в компоненте
Типизация в TypeScriptСложная — нужно пробрасывать дженерикиПростая — обычные типы
ВложенностьГлубокая вложенность обёртокПлоский код, композиция через вызовы
DevTools«Обёртка в обёртке», нужна displayNameЧистое дерево компонентов
Статические методыТеряются, нужен hoist-non-react-staticsНе проблема
Совместимость с классамиРаботает с классами и функциямиТолько функциональные компоненты
Реальные библиотекиReact.memo, connect (Redux), withRouterПрактически все современные библиотеки

Почему хуки вытеснили HOC

  1. Прозрачность данных. В хуке значение — локальная переменная, видно откуда взялось. В HOC — «магический» пропс, пришедший неизвестно откуда.
  2. Отсутствие обёрток. Хук не создаёт дополнительный компонент в дереве — нет проблем с DevTools и DOM-структурой.
  3. Плоская композиция. Несколько хуков вызываются последовательно, без вложенности.
  4. Лучшая типизация. TypeScript работает естественно, без дженерик-конструкций.

HOC vs Render Props

Render Props — ещё один паттерн переиспользования логики, популярный в переходный период между HOC и хуками. Вместо обёртки компонента HOC передаёт логику через проп-функцию:

// Render Props
function WindowSize({ children }) {
  const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
  // ...подписка на resize...
  return children(size);
}

// Использование
<WindowSize>
  {({ width, height }) => <div>{width}x{height}</div>}
</WindowSize>

Сравнение

КритерийHOCRender Props
Структура дереваДобавляет обёрткуНе добавляет (рендерит children)
Именование пропсовHOC решает, какие пропсы добавитьПользователь решает через деструктуризацию
JSX-вложенностьНе нужнаМожет привести к «advent of Christmas» — глубокой вложенности render props
ТипизацияСложнаяПроще, но всё ещё требует дженериков

Оба паттерна в 2026 году в значительной степени заменены хуками, но встречаются в legacy-коде и некоторых библиотеках.


Vue-аналоги

Во Vue паттерн HOC существует, но используется крайне редко. Основные механизмы переиспользования логики:

Композаблы — современный аналог

<script setup>
import { ref, onMounted, onUnmounted } from "vue";

export function useWindowSize() {
  const width = ref(window.innerWidth);
  const height = ref(window.innerHeight);

  function onResize() {
    width.value = window.innerWidth;
    height.value = window.innerHeight;
  }

  onMounted(() => window.addEventListener("resize", onResize));
  onUnmounted(() => window.removeEventListener("resize", onResize));

  return { width, height };
}
</script>

Миксины — legacy-аналог HOC

// Миксин — объект с опциями, подмешиваемый в компонент
const windowSizeMixin = {
  data() {
    return { width: window.innerWidth, height: window.innerHeight };
  },
  mounted() {
    window.addEventListener("resize", this.onResize);
  },
  unmounted() {
    window.removeEventListener("resize", this.onResize);
  },
  methods: {
    onResize() {
      this.width = window.innerWidth;
      this.height = window.innerHeight;
    },
  },
};

export default {
  mixins: [windowSizeMixin],
  // ...
};

Миксины имеют серьёзные проблемы (неявные источники данных, конфликты имён), поэтому в Vue 3 предпочтительны композаблы — функционально эквивалентные React-хукам.

HOC во Vue (технически возможен)

import { defineComponent, h } from "vue";

function withAuth(WrappedComponent) {
  return defineComponent({
    setup(props, { attrs, slots }) {
      const user = useAuth();
      if (!user) return () => h(Redirect, { to: "/login" });
      return () => h(WrappedComponent, { ...props, ...attrs, user }, slots);
    },
  });
}

На практике это не нужно — композаблы покрывают те же сценарии проще и прозрачнее.


Когда HOC всё ещё актуален

Несмотря на доминирование хуков, HOC остаётся востребованным в нескольких сценариях:

1. Встроенные HOC React

  • React.memo — мемоизация компонентов.
  • React.forwardRef — проброс ref через обёртку.
  • React.lazy — ленивая загрузка компонентов.

Эти HOC — часть API React и не имеют альтернатив в виде хуков.

2. Библиотеки, поддерживающие классовые компоненты

Redux connect, React Router withRouter и другие библиотеки эпохи классов продолжают использовать HOC для обратной совместимости. В новых проектах они заменены хуками (useSelector, useNavigate), но в legacy-коде встречаются повсеместно.

3. Поведенческие обёртки, не связанные с состоянием

HOC удобен, когда нужно добавить компоненту поведение, не требующее собственного состояния:

// Добавление data-testid ко всем элементам для тестирования
function withTestId(WrappedComponent, prefix) {
  return function WithTestId(props) {
    return <WrappedComponent {...props} data-testid={`${prefix}-${props.id}`} />;
  };
}

4. Интеграция с системами, работающими на уровне компонентов

Некоторые системы (Error Boundaries, Suspense-обёртки) требуют компонентного уровня — их нельзя реализовать хуком. HOC здесь естественный выбор:

function withErrorBoundary(WrappedComponent, fallback) {
  return functionWithErrorBoundary(props) {
    return (
      <ErrorBoundary fallback={fallback}>
        <WrappedComponent {...props} />
      </ErrorBoundary>
    );
  };
}

Ключевые тезисы для интервью

  • HOC — функция, принимающая компонент и возвращающая новый компонент с расширенным поведением.
  • HOC не мутирует исходный компонент, а создаёт обёртку; пропсы передаются через.
  • displayName обязателен для читаемого дерева в DevTools.
  • Композиция HOC через compose инвертирует порядок вложенности для читаемости.
  • Порядок композиции влияет на поведение: withAuth(withData(...)) ≠ withData(withAuth(...)).
  • Хуки вытеснили HOC для переиспользования логики: прозрачность данных, плоская композиция, лучшая типизация.
  • Встроенные HOC React (memo, forwardRef, lazy) остаются без альтернатив в виде хуков.
  • HOC внутри рендера — антипаттерн: приводит к размонтированию и потере состояния.

Заключение

Higher-Order Components — важный паттерн в истории React, решавший задачу переиспользования логики до появления хуков. В 2026 году HOC занял свою нишу: встроенные React.memo, React.forwardRef, React.lazy для мемоизации и ленивой загрузки, Error Boundary и Suspense-обёртки. Для переиспользования логики с состоянием используйте хуки. В legacy-коде HOC встречается часто — понимание паттерна обязательно для чтения чужого кода.

Полезные ссылки