Постепенное внедрение

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

Создание компонента

Компонент в Haunted определяется как функция, которая возвращает разметку через lit-html шаблоны. Ключевой инструмент — функция component из библиотеки.

import { component, html, useState } from 'haunted';

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

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

Пояснения:

  • useState — хук состояния. Возвращает текущее значение и функцию для его обновления.
  • html — функция шаблона, которая создаёт реактивную разметку.
  • @click — синтаксис для привязки событий в Haunted (на основе lit-html).

Управление состоянием

Состояние компонента в Haunted полностью реактивное. Изменение значения через setState вызывает повторный рендер компонента. Можно использовать несколько состояний одновременно:

function MultiCounter() {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  return html`
    <div>
      <p>A: ${countA}</p>
      <p>B: ${countB}</p>
      <button @click=${() => setCountA(countA + 1)}>Увеличить A</button>
      <button @click=${() => setCountB(countB + 1)}>Увеличить B</button>
    </div>
  `;
}

customElements.define('multi-counter', component(MultiCounter));

Использование эффектов

Хуки эффектов (useEffect) позволяют выполнять побочные действия, аналогично React. Основные сценарии: подписки, таймеры, запросы к API.

import { useEffect } from 'haunted';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(interval); // Очистка при размонтировании
  }, []); // Пустой массив зависимостей — эффект выполняется один раз

  return html`<p>Прошло секунд: ${seconds}</p>`;
}

customElements.define('my-timer', component(Timer));

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

  • Функция, возвращаемая из useEffect, используется для очистки ресурсов.
  • Массив зависимостей определяет, когда эффект должен перезапускаться. Если массив пустой, эффект выполняется только при монтировании.

Передача свойств

Haunted поддерживает реактивные свойства (props), которые передаются через атрибуты HTML.

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

customElements.define('my-greeting', component(Greeting));

Использование:

<my-greeting name="Алексей"></my-greeting>

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

  • Свойства компонента автоматически обновляют рендер при изменении.
  • Для передачи сложных объектов и массивов необходимо использовать JavaScript-синтаксис:
document.querySelector('my-greeting').name = { first: 'Алексей', last: 'Иванов' };

Хуки дополнительных возможностей

Haunted предоставляет несколько стандартных хуков:

  • useReducer — управление состоянием через редьюсер, полезно при сложной логике.
  • useRef — хранение ссылки на DOM-элемент между рендерами.
  • useContext — создание контекста и совместное использование состояния между компонентами.

Пример useReducer:

import { useReducer } from 'haunted';

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    default: return state;
  }
}

function ReducerCounter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return html`
    <p>Счётчик: ${state.count}</p>
    <button @click=${() => dispatch({ type: 'increment' })}>+</button>
    <button @click=${() => dispatch({ type: 'decrement' })}>-</button>
  `;
}

customElements.define('reducer-counter', component(ReducerCounter));

Интеграция с другими библиотеками

Haunted совместим с любыми библиотеками, которые работают с нативными DOM-элементами. Можно легко подключать стили через CSS-in-JS или lit-element шаблоны, а также использовать сторонние UI-компоненты.

Пример интеграции с кастомным элементом:

function FancyButton({ label }) {
  return html`<button class="fancy">${label}</button>`;
}

customElements.define('fancy-button', component(FancyButton));

function App() {
  return html`<fancy-button label="Нажми меня"></fancy-button>`;
}

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

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

  • Haunted минимизирует перерисовки, сравнивая предыдущий и новый шаблон через lit-html.
  • Изменения состояния вызывают только локальный рендер компонента, не затрагивая соседние элементы.
  • Эффекты и подписки нужно очищать, чтобы избежать утечек памяти при динамическом создании компонентов.

Внедрение постепенно

Haunted идеально подходит для постепенного внедрения в существующие проекты. Можно добавлять новые компоненты по мере необходимости, оставляя остальной код на чистом HTML/JS. Компоненты остаются совместимыми с любыми браузерами, поддерживающими Web Components, что позволяет интегрировать Haunted в проект без полной переработки архитектуры.

  • Можно начинать с отдельных виджетов или UI-блоков.
  • Интеграция не требует глобального состояния, так как каждый компонент сам управляет своими хуками.
  • Постепенное добавление Haunted не ломает существующий функционал, а улучшает структуру и читаемость кода.