Изоморфный код

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

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

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));

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

  • html — функция-шаблонизатор для создания DOM.
  • useState — хук состояния, аналогичный React.
  • Обновление DOM происходит автоматически при изменении состояния.

Хуки в Haunted

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

  • useState(initialValue) — хранение локального состояния.
  • useEffect(callback, dependencies) — выполнение побочных эффектов при изменении зависимостей.
  • useReducer(reducer, initialState) — управление сложным состоянием.
  • useRef(initialValue) — создание ссылки на DOM или любое значение, сохраняющееся между рендерами.
  • useContext(context) — доступ к контексту.

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

import { component, html, useState, 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));

Изоморфный код

Изоморфизм в контексте Haunted означает возможность использовать один и тот же компонентный код как на сервере, так и на клиенте, с корректной генерацией HTML и поддержкой интерактивности после гидратации. Haunted поддерживает серверный рендеринг через функцию render из пакета haunted/ssr.

Серверный рендеринг

import { render } from 'haunted/ssr';
import { Counter } from './components/counter.js';

const htmlString = render(Counter);
console.log(htmlString);

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

  • render возвращает строку HTML, которая может быть вставлена в шаблон сервера.
  • При рендеринге на сервере хуки состояния и эффекты не выполняются, что обеспечивает безопасное выполнение.

Гидратация на клиенте

Чтобы сделать серверный HTML интерактивным на клиенте, используется обычная регистрация компонента через customElements.define:

import { component } from 'haunted';
import { Counter } from './components/counter.js';

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

После загрузки страницы браузер «гидрирует» серверный HTML, связывая его с функциональностью Haunted-компонента. Важно, что структура DOM, сгенерированная сервером, должна точно соответствовать структуре, которую создаст клиентский компонент.

Практика изоморфного кода

  1. Изоляция логики состояния — состояние и эффекты компонента должны быть детерминированными и не зависящими от глобальных объектов браузера при рендеринге на сервере.
  2. Обеспечение согласованного DOM — разметка, возвращаемая функцией компонента, должна быть одинаковой на сервере и клиенте.
  3. Ленивая инициализация эффектов — любые действия с window, document или сторонними библиотеками должны выполняться только на клиенте внутри useEffect.

Пример условной работы с браузерными объектами:

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

function WindowWidth() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    function updateWidth() {
      setWidth(window.innerWidth);
    }
    updateWidth();
    window.addEventListener('resize', updateWidth);
    return () => window.removeEventListener('resize', updateWidth);
  }, []);

  return html`<p>Ширина окна: ${width}</p>`;
}

customElements.define('window-width', component(WindowWidth));

На сервере значение width останется 0, а на клиенте произойдет корректная гидратация и последующее обновление при изменении размера окна.

Интеграция с маршрутизаторами и фреймворками

Haunted-компоненты легко интегрируются с серверными фреймворками, такими как Express, Next.js или Fastify. Основной подход:

  1. Использовать render для генерации HTML на сервере.
  2. Встраивать сгенерированную строку в шаблон страницы.
  3. На клиенте регистрировать компоненты для гидратации.

Пример с Express:

import express from 'express';
import { render } from 'haunted/ssr';
import { Counter } from './components/counter.js';

const app = express();

app.get('/', (req, res) => {
  const htmlContent = render(Counter);
  res.send(`
    <html>
      <body>
        ${htmlContent}
        <script type="module" src="/client.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

В /client.js будет код:

import { component } from 'haunted';
import { Counter } from './components/counter.js';

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

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

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

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