Higher-Order Components

Higher-Order Components (HOC) — это архитектурный приём, при котором функция принимает компонент и возвращает новый компонент с расширенным поведением. В Stencil данный подход применяется не в классическом виде, как в React, а адаптируется под специфику компилятора, Web Components и декораторной модели.

Stencil ориентирован на генерацию стандартных Web Components, поэтому HOC здесь не являются частью официального API, но активно используются как паттерн для переиспользования логики, кросс-компонентных возможностей и инкапсуляции поведения.


Отличие HOC в Stencil от React-подхода

Ключевые различия обусловлены архитектурой:

  • отсутствует виртуальный DOM как основной контракт взаимодействия;
  • компоненты компилируются в нативные Custom Elements;
  • жизненный цикл жёстко определён и не может быть произвольно обёрнут;
  • свойства и события декларируются через декораторы.

В результате Higher-Order Components в Stencil чаще реализуются как:

  • фабрики компонентов;
  • функции-обёртки над классом компонента;
  • миксины;
  • вспомогательные функции, возвращающие модифицированный класс.

Базовая идея HOC в Stencil

Общая схема:

  1. Исходный компонент описывается как класс.
  2. Функция принимает этот класс.
  3. Возвращается новый класс, расширяющий исходный или модифицирующий его поведение.

Пример концептуальной структуры:

type StencilComponent<T> = new (...args: any[]) => T;

function withSomething<T extends StencilComponent<any>>(Base: T) {
  return class extends Base {
    // дополнительная логика
  };
}

Такой подход позволяет внедрять повторяющееся поведение без копирования кода.


Использование HOC для расширения жизненного цикла

Stencil предоставляет фиксированный набор хуков жизненного цикла:

  • componentWillLoad
  • componentDidLoad
  • componentWillRender
  • componentDidRender
  • componentWillUpdate
  • componentDidUpdate
  • disconnectedCallback

HOC может перехватывать эти методы, сохраняя оригинальное поведение.

function withLogging<T extends new (...args: any[]) => any>(Base: T) {
  return class extends Base {
    async componentWillLoad() {
      if (super.componentWillLoad) {
        await super.componentWillLoad();
      }
      console.log('Component will load');
    }
  };
}

Ключевые моменты:

  • обязательная проверка существования метода у super;
  • сохранение асинхронности;
  • отсутствие изменения публичного API компонента.

Добавление общего состояния через HOC

Stencil не поддерживает контекст в стиле React, но HOC может внедрять состояние через @State.

import { State } from '@stencil/core';

function withLoadingState<T extends new (...args: any[]) => any>(Base: T) {
  return class extends Base {
    @State() loading = false;

    setLoading(value: boolean) {
      this.loading = value;
    }
  };
}

Результат:

  • компонент получает новое реактивное состояние;
  • обновления автоматически вызывают ререндер;
  • базовый компонент не знает о происхождении состояния.

Инъекция свойств и поведения

HOC может добавлять вычисляемые свойства, методы и вспомогательную логику.

Пример внедрения форматирования данных:

function withFormatter<T extends new (...args: any[]) => any>(Base: T) {
  return class extends Base {
    formatDate(date: Date): string {
      return date.toISOString().split('T')[0];
    }
  };
}

Такой подход особенно полезен для:

  • UI-компонентов с повторяющимися правилами отображения;
  • библиотек компонентов;
  • дизайн-систем.

Композиция нескольких Higher-Order Components

HOC легко комбинируются:

const EnhancedComponent = withLogging(
  withLoadingState(
    withFormatter(MyComponent)
  )
);

При этом важно учитывать порядок:

  • внешние HOC оборачивают внутренние;
  • конфликт имён методов приводит к переопределению;
  • жизненный цикл выполняется сверху вниз по цепочке наследования.

HOC и декораторы Stencil

Stencil использует декораторы:

  • @Component
  • @Prop
  • @State
  • @Event
  • @Method
  • @Watch

HOC не может динамически добавлять декораторы после компиляции. Все декораторы должны быть известны компилятору на этапе анализа кода.

Следствия:

  • @Prop нельзя добавить условно;
  • HOC должен быть применён до использования класса в @Component;
  • возвращаемый класс обязан быть окончательным компонентом.

Корректный порядок:

const Enhanced = withLoadingState(BaseComponent);

@Component({
  tag: 'my-component',
  shadow: true
})
export class MyComponent extends Enhanced {}

HOC как альтернатива миксинам

Миксины в TypeScript часто используются в Stencil. HOC фактически является их функциональным вариантом.

Сравнение:

Миксин

  • требует сложных типов;
  • менее читаем;
  • статическая композиция.

HOC

  • декларативный стиль;
  • проще комбинировать;
  • удобнее для условного расширения.

Оба подхода могут использоваться совместно.


Ограничения и риски использования HOC

Основные ограничения:

  • усложнение цепочки наследования;
  • ухудшение читаемости итогового класса;
  • сложность отладки;
  • невозможность динамического добавления @Prop и @Event.

Рекомендации:

  • применять HOC для инфраструктурной логики;
  • избегать бизнес-логики внутри HOC;
  • документировать добавляемые свойства и методы;
  • не использовать HOC для визуальных изменений.

Практические сценарии применения

Типичные кейсы:

  • логирование жизненного цикла;
  • подключение аналитики;
  • обработка ошибок;
  • управление загрузкой данных;
  • унификация API компонентов;
  • интеграция с внешними сервисами.

HOC особенно эффективны в:

  • UI-библиотеках;
  • дизайн-системах;
  • корпоративных фреймворках на базе Stencil.

Архитектурная роль HOC в Stencil-проектах

Higher-Order Components формируют слой абстракции между инфраструктурой и конкретными компонентами. Они позволяют:

  • снижать дублирование;
  • централизовать кросс-компонентное поведение;
  • поддерживать единые стандарты;
  • масштабировать кодовую базу без жёсткой связности.

В экосистеме Stencil HOC не являются обязательным инструментом, но при грамотном использовании становятся мощным архитектурным приёмом, дополняющим декораторы, миксины и сервисные слои.