Композиция событий

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


События и их обработка

В Haunted события представляют собой стандартные DOM-события, к которым можно привязать обработчики через хуки или напрямую в шаблоне:

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

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

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

  return html`
    <button @click=${increment}>${count}</button>
  `;
}

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

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


Хуки для событий

Haunted предоставляет хуки useEffect и useEvent (через расширения) для работы с событиями на более абстрактном уровне. useEffect позволяет подписываться на события при монтировании компонента и корректно удалять обработчики при размонтировании:

import { useEffect } from 'haunted';

function Toggle() {
  const [on, setOn] = useState(false);

  useEffect(() => {
    const handleKey = e => {
      if (e.key === 'Enter') setOn(!on);
    };
    window.addEventListener('keydown', handleKey);
    return () => window.removeEventListener('keydown', handleKey);
  }, [on]);

  return html`<div>${on ? 'Включено' : 'Выключено'}</div>`;
}

Особенность: Возврат функции из useEffect обеспечивает автоматическую очистку обработчиков, что предотвращает утечки памяти и дублирование событий при повторном рендеринге.


Композиция нескольких событий

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

function MultiEventButton() {
  const [state, setState] = useState('idle');

  const handleClick = () => setState('clicked');
  const handleMouseOver = () => setState('hovered');

  const composedHandler = e => {
    handleClick(e);
    handleMouseOver(e);
  };

  return html`<button @click=${composedHandler} @mouseover=${composedHandler}>${state}</button>`;
}

Важный момент: Композиция не должна создавать циклические обновления состояния. Нужно контролировать порядок вызовов и их влияние на реактивные переменные.


Применение утилит для композиции

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

const composeEvents = (...handlers) => e => handlers.forEach(h => h && h(e));

function Example() {
  const [count, setCount] = useState(0);
  const [hovered, setHovered] = useState(false);

  const increment = () => setCount(c => c + 1);
  const hover = () => setHovered(true);

  return html`
    <button @click=${composeEvents(increment, hover)}>Счет: ${count}, Hovered: ${hovered}</button>
  `;
}

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


Работа с кастомными событиями

Haunted полностью поддерживает кастомные события через стандартный CustomEvent. Это особенно важно для создания компонентов, которые обмениваются данными с внешними частями приложения:

function Child() {
  const notifyParent = () => {
    dispatchEvent(new CustomEvent('child-clicked', {
      detail: { time: Date.now() },
      bubbles: true,
      composed: true
    }));
  };

  return html`<button @click=${notifyParent}>Notify parent</button>`;
}

function Parent() {
  const [lastClick, setLastClick] = useState(null);

  const handleChildClick = e => setLastClick(e.detail.time);

  useEffect(() => {
    window.addEventListener('child-clicked', handleChildClick);
    return () => window.removeEventListener('child-clicked', handleChildClick);
  }, []);

  return html`<div>
    <child-component></child-component>
    <p>Last child click: ${lastClick}</p>
  </div>`;
}

Особенности кастомных событий:

  • bubbles: true позволяет событию подниматься вверх по DOM.
  • composed: true разрешает проход события через Shadow DOM.

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

  1. Локальная композиция: объединение нескольких обработчиков внутри одного компонента.
  2. Глобальная композиция: события поднимаются на уровень приложения через кастомные события.
  3. Комбинированная: микс локальных и глобальных событий с контролем состояния через хуки.

Контроль порядка и предотвращение конфликтов

При композиции важно:

  • Определять порядок вызова обработчиков, чтобы состояние изменялось предсказуемо.
  • Использовать stopPropagation() только там, где необходимо, иначе можно блокировать важные события.
  • Избегать изменения одного и того же состояния из разных обработчиков без условий.
const safeCompose = (...handlers) => e => {
  for (const h of handlers) {
    if (h) h(e);
    if (e.defaultPrevented) break;
  }
};

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

  • Минимизировать количество подписок через useEffect.
  • Использовать мемоизацию функций через useCallback для стабильности ссылок на обработчики.
  • Предпочитать локальные события компонентам вместо глобальных, если это возможно.

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