Паттерны композиции

Haunted — это библиотека для создания веб-компонентов на основе реактивного программирования в стиле Hooks, вдохновлённого React. Основной акцент делается на легковесность, композицию функций и повторное использование логики. В отличие от стандартных веб-компонентов, Haunted позволяет строить сложные интерфейсы, используя чистые функции и хуки, без необходимости наследовать HTMLElement и управлять жизненным циклом вручную.


Создание функционального компонента

Функциональный компонент в Haunted — это обычная JavaScript-функция, возвращающая шаблон разметки, создаваемой с помощью html (из haunted):

import { html, component } from 'haunted';

function Counter({ initial = 0 }) {
  const [count, setCount] = useState(initial);

  return html`
    <div>
      <p>Счётчик: ${count}</p>
      <button @click=${() => setCount(count + 1)}>Увеличить</button>
    </div>
  `;
}

customElements.define('my-counter', component(Counter));

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

  • useState создаёт реактивное состояние внутри компонента.
  • component() оборачивает функцию в веб-компонент, автоматически управляя обновлением DOM при изменении состояния.
  • html используется для шаблонов с динамическим содержимым.

Композиция через вложенные компоненты

Haunted делает возможной глубокую композицию компонентов, где один компонент может содержать другой:

function App() {
  return html`
    <div>
      <h1>Главный интерфейс</h1>
      <my-counter initial="5"></my-counter>
      <my-counter initial="10"></my-counter>
    </div>
  `;
}

customElements.define('my-app', component(App));

Особенности:

  • Компоненты могут быть многократно использованы с разными параметрами.
  • Поддерживается изоляция состояния: каждый экземпляр хранит собственные значения хуков.
  • Props передаются через атрибуты или объект параметров при вызове функции.

Повторное использование логики с хуками

Haunted поддерживает создание кастомных хуков, что позволяет инкапсулировать повторяемую реактивную логику:

function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return { count, increment, decrement };
}

function CounterControls() {
  const { count, increment, decrement } = useCounter(3);

  return html`
    <div>
      <button @click=${decrement}>-</button>
      <span>${count}</span>
      <button @click=${increment}>+</button>
    </div>
  `;
}

customElements.define('counter-controls', component(CounterControls));

Паттерн повторного использования логики:

  • Хуки позволяют вынести состояние и методы управления из компонента, делая код чистым.
  • Логика не привязана к конкретной разметке — её можно использовать в разных компонентах.
  • Поддерживается чистый реактивный стиль, где изменение состояния автоматически обновляет DOM.

Передача и реакция на свойства

Haunted предоставляет возможность реактивного обновления компонентов при изменении атрибутов:

function Greeting({ name }) {
  return html`<p>Привет, ${name}!</p>`;
}

customElements.define('greeting-element', component(Greeting));
  • Любое изменение атрибута name автоматически вызовет перерендер.
  • Атрибуты доступны через объект пропсов, а состояние можно комбинировать с хуками.

Расширенные техники:

  • Использование useEffect для выполнения побочных эффектов при изменении props или состояния.
  • Вложенные хуки позволяют строить сложные реактивные зависимости без громоздкого кода.

Паттерн “Слоты и композиция разметки”

Haunted интегрируется с нативной системой слотов веб-компонентов, позволяя композицию пользовательской разметки:

function Card({ title }) {
  return html`
    <div class="card">
      <h2>${title}</h2>
      <slot></slot>
    </div>
  `;
}

customElements.define('custom-card', component(Card));
  • <slot> обеспечивает вставку произвольного контента внутрь компонента.
  • Паттерн слотов отлично сочетается с хуками, позволяя создавать универсальные обертки для UI-элементов.

Паттерн “Композиция через функции-обертки”

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

function withBorder(Component) {
  return function(props) {
    return html`<div style="border: 2px solid black; padding: 10px;">
      <${Component} ...=${props}></${Component}>
    </div>`;
  };
}

const BorderedCounter = withBorder(Counter);
customElements.define('bordered-counter', component(BorderedCounter));
  • Позволяет добавлять функциональность без изменения оригинального компонента.
  • Поддерживает вложенность и композицию, аналогично HOC (Higher-Order Components) в React.
  • Упрощает применение единого стиля или поведения для группы компонентов.

Рекомендации по архитектуре

  • Разделять состояние и представление: использовать хуки для управления состоянием, компоненты — для разметки.
  • Мелкие переиспользуемые компоненты: легче комбинировать и тестировать.
  • Композиция через функции и слоты: обеспечивает гибкость интерфейса и адаптацию под разные сценарии.
  • Избегать глобального состояния внутри компонентов: лучше передавать данные через props или кастомные хуки.

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