Миграция с React

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

Ключевые моменты, на которые следует обратить внимание при миграции с React:

  • Функциональные компоненты в Haunted определяются как обычные функции, возвращающие шаблонный HTML через html из haunted.
  • Hooks работают аналогично React: useState, useEffect, useRef, useReducer, но применяются в контексте функции веб-компонента.
  • Обновления DOM происходят автоматически при изменении состояния, благодаря реактивной системе Haunted, без необходимости использования виртуального DOM.

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

В React:

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

  return (
    <div>
      <p>{count}</p>
      <button onCl ick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

В Haunted:

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

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

  return html`
    <div>
      <p>${count}</p>
      <button @click=${() => setCount(count + 1)}>Increment</button>
    </div>
  `;
}

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

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

  • События в Haunted используют @ вместо on, например @click=${handler}.
  • component() оборачивает функцию и превращает её в полноценный веб-компонент.
  • JSX отсутствует, используется шаблонный литерал html.

Работа с состоянием

Haunted поддерживает несколько видов состояний и их обновлений:

  • useState(initialValue) — базовое состояние, аналог React.
  • useReducer(reducer, initialState) — для сложной логики управления состоянием.
  • useRef(initialValue) — хранение изменяемых данных без триггера обновлений DOM.

Пример использования useReducer:

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

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

function counter() {
  const [count, dispatch] = useReducer(reducer, 0);

  return html`
    <div>
      <p>${count}</p>
      <button @click=${() => dispatch({ type: 'increment' })}>+</button>
      <button @click=${() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  `;
}

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

Эффекты и жизненный цикл

Haunted предоставляет хук useEffect, аналогичный React. Его особенности:

  • Первым аргументом передаётся функция с побочными эффектами.
  • Вторым аргументом — массив зависимостей, обновление происходит только при изменении этих зависимостей.
  • Функция может возвращать функцию очистки.

Пример:

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

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

  useEffect(() => {
    const id = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return html`<p>Time: ${seconds}s</p>`;
}

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

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

Работа с пропсами и атрибутами

В Haunted props передаются как обычные атрибуты веб-компонентов. Они доступны в функции компонента через аргумент props.

Пример:

function Greeting({ name }) {
  return html`<p>Hello, ${name}!</p>`;
}

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

При использовании:

<my-greeting name="Alice"></my-greeting>

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

  • Props реактивны. Изменение атрибута компонента обновляет рендер.
  • Атрибуты преобразуются к строкам. Для передачи объектов рекомендуется использовать свойства через JS:
const el = document.createElement('my-greeting');
el.name = 'Bob';
document.body.appendChild(el);

Контекст и совместное состояние

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

  • Пользовательские события (CustomEvent) и слушатели.
  • Глобальные реактивные объекты через useState или сторонние состояния, например, valtio или zustand.
  • Shadow DOM не мешает прокидыванию событий, что позволяет использовать обычные DOM-паттерны для передачи данных.

Миграция компонентов

При переносе React-компонентов в Haunted следует учитывать:

  • JSX → html-теги. Все JSX-синтаксис преобразуется в шаблонные литералы.
  • События: onClick@click, onChange@change и т.д.
  • useState и useEffect полностью сохраняют логику, но useEffect выполняется в контексте веб-компонента.
  • Refs: useRef работает только для хранения данных, привязка к DOM через ref.current невозможна, используйте стандартные querySelector в Shadow DOM.
  • Переиспользуемые хуки: все хуки React можно реализовать через аналоги Haunted, при этом синтаксис остаётся почти идентичным.

Shadow DOM и стили

Haunted по умолчанию использует Shadow DOM, что изолирует стили. Для миграции из React:

  • Стили, подключаемые через CSS-файлы, нужно либо инкапсулировать в Shadow DOM, либо использовать CSS-переменные.
  • Для глобальных стилей можно применять :host или вставлять <style> внутри компонента.
  • Scoped CSS помогает избежать конфликтов с остальной частью страницы, чего нет в React без CSS-модулей.

Рендеринг списков и условный рендеринг

В Haunted списки рендерятся с помощью map внутри шаблонного литерала:

function ItemList({ items }) {
  return html`
    <ul>
      ${items.map(item => html`<li>${item}</li>`)}
    </ul>
  `;
}

customElements.define('item-list', component(ItemList));

Условный рендеринг выполняется через тернарные операторы:

const loggedIn = true;
return html`
  ${loggedIn ? html`<p>Welcome back!</p>` : html`<p>Please login</p>`}
`;

Асинхронные операции и эффекты

Асинхронные действия, такие как запросы к API, выполняются в useEffect. В Haunted можно использовать async-функции, но внутри эффекта их нужно вызывать корректно:

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

  async function fetchData() {
    const response = await fetch('/api/data');
    const data = await response.json();
    if (!cancelled) setData(data);
  }

  fetchData();

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

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

Ключевые советы при миграции

  • Минимизировать использование внешних зависимостей, полагаясь на нативные возможности веб-компонентов.
  • Проверять совместимость событий и Shadow DOM, так как некоторые глобальные библиотеки могут не работать.
  • Оптимизировать рендеринг списков, избегая лишних вычислений в map.
  • Придерживаться реактивного подхода с хуками — это основной паттерн в Haunted.

Haunted позволяет плавно перенести большинство концепций React, сохранив знакомую работу с хуками, состоянием и эффектами, но при этом использовать преимущества нативных веб-компонентов и Shadow DOM для более компактного и быстрого кода.