Множественные рендеры

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


Основы рендера

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

import { html, component } from 'haunted';

function Counter({ value = 0 }) {
  return html`
    <div>
      <p>Значение: ${value}</p>
    </div>
  `;
}

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

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


Множественные вызовы render

При работе с несколькими экземплярами компонента или повторным вызовом рендера одного и того же компонента важно понимать, что:

  1. Каждый вызов component() создаёт новый класс веб-компонента.
  2. Использование одного и того же тега в разных местах страницы не пересоздаёт компонент, но инициирует его отдельные рендеры.
  3. Повторные вызовы функции компонента с разными пропсами не влияют на другие экземпляры, так как состояние привязано к конкретному элементу.

Пример множественных рендеров:

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

function MultiCounter() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return html`
    <div>
      <button @click=${() => setCount1(count1 + 1)}>Increment 1</button>
      <p>Первый счётчик: ${count1}</p>

      <button @click=${() => setCount2(count2 + 1)}>Increment 2</button>
      <p>Второй счётчик: ${count2}</p>
    </div>
  `;
}

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

Каждое нажатие кнопки вызывает локальный рендер соответствующего счётчика, не затрагивая другой.


Оптимизация множественных рендеров

Haunted предоставляет несколько подходов для минимизации избыточных рендеров:

  • Разделение состояния: отдельные useState или useReducer для разных частей интерфейса. Это предотвращает ненужное обновление всего компонента при изменении одной части состояния.
  • Мемоизация шаблонов через useMemo или cache:
import { html, component, useMemo, useState } from 'haunted';

function ExpensiveRender({ data }) {
  const expensiveTemplate = useMemo(() => {
    return html`<p>Обработанные данные: ${data.map(d => d * 2)}</p>`;
  }, [data]);

  return html`<div>${expensiveTemplate}</div>`;
}

customElements.define('expensive-render', component(ExpensiveRender));

useMemo предотвращает пересоздание тяжёлого шаблона, если зависимости не изменились.

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

Синхронные и асинхронные рендеры

Haunted поддерживает работу с асинхронными данными напрямую в шаблонах:

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

function AsyncDataComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://api.example.com/items')
      .then(res => res.json())
      .then(setData);
  }, []);

  return html`
    <div>
      ${data ? html`<ul>${data.map(item => html`<li>${item.name}</li>`)}</ul>` : html`<p>Загрузка...</p>`}
    </div>
  `;
}

customElements.define('async-data', component(AsyncDataComponent));

Каждое изменение состояния data инициирует отдельный рендер, при этом Haunted обновляет только необходимые узлы DOM.


Контроль рендера через shouldUpdate

Haunted позволяет ограничивать рендеры через переопределение метода shouldUpdate в компоненте:

function ConditionalRender({ value }) {
  return html`<p>Значение: ${value}</p>`;
}

const ConditionalComponent = component(ConditionalRender, { 
  observedAttributes: ['value'],
  shouldUpdate: (el, changedProps) => changedProps.has('value') && changedProps.get('value') % 2 === 0
});

customElements.define('conditional-render', ConditionalComponent);

В этом примере компонент будет обновляться только тогда, когда value чётное, предотвращая лишние рендеры при других изменениях.


Интерактивные списки и множественные рендеры

При работе с динамическими списками особенно важно контролировать идентичность элементов. Haunted использует ключи (key) через repeat из lit-html:

import { html, component } from 'haunted';
import { repeat } from 'lit-html/directives/repeat.js';

function TodoList({ items }) {
  return html`
    <ul>
      ${repeat(items, item => item.id, item => html`<li>${item.text}</li>`)}
    </ul>
  `;
}

customElements.define('todo-list', component(TodoList));

repeat обеспечивает минимальное количество изменений DOM при добавлении, удалении или обновлении элементов списка, даже если компонент рендерится многократно.


Резюме ключевых принципов множественных рендеров

  • Каждый экземпляр компонента имеет своё локальное состояние, независимое от других.
  • Разделение состояния и компонентов снижает нагрузку при множественных рендерах.
  • Использование useMemo, repeat и shouldUpdate позволяет оптимизировать производительность.
  • Асинхронные изменения состояния автоматически инициируют рендер, затрагивая только изменившиеся узлы DOM.

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