Производные значения

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

Концепция производных значений

Производные значения (derived values) представляют собой вычисляемые состояния, которые зависят от одного или нескольких реактивных состояний (state). Основное отличие от обычных переменных в том, что они автоматически пересчитываются, когда изменяются зависимые значения. Это избавляет от необходимости вручную синхронизировать интерфейс с данными.

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

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

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

  const double = useMemo(() => count * 2, [count]);

  return html`
    <div>Счётчик: ${count}</div>
    <div>Удвоенное значение: ${double}</div>
    <button @click=${() => setCount(count + 1)}>Увеличить</button>
  `;
}

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

В этом примере double является производным значением, которое автоматически пересчитывается при изменении count. Массив зависимостей [count] указывает, от каких состояний зависит вычисление.

Определение зависимостей

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

const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);

Важным аспектом является правильное указание зависимостей. Пропуск зависимой переменной может привести к несвоевременному обновлению интерфейса, а избыточные зависимости — к лишним пересчётам.

Производные значения с асинхронными вычислениями

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

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

function AsyncData({ url }) {
  const [data, setData] = useState(null);

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

    async function fetchData() {
      const response = await fetch(url);
      const result = await response.json();
      if (!cancelled) setData(result);
    }

    fetchData();

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

  return html`
    <div>${data ? JSON.stringify(data) : 'Загрузка...'}</div>
  `;
}

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

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

Композиция производных значений

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

const a = useState(1);
const b = useMemo(() => a * 2, [a]);
const c = useMemo(() => b + 3, [b]);

В этом примере c автоматически обновляется при изменении a, потому что b также обновляется и является зависимостью c.

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

Использование useMemo помогает избежать ненужных пересчётов при рендере. Для сложных вычислений, таких как фильтрация массивов или сложные математические операции, это критично для производительности.

const filteredItems = useMemo(
  () => items.filter(item => item.active),
  [items]
);

Здесь фильтрация выполняется только при изменении массива items, что снижает нагрузку на DOM при частых рендерах.

Особенности интеграции с Haunted

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

Примеры практического использования

  1. Фильтрация и сортировка данных:
const sortedData = useMemo(
  () => data.slice().sort((a, b) => a.value - b.value),
  [data]
);
  1. Вычисление сумм или агрегатов:
const total = useMemo(
  () => cartItems.reduce((sum, item) => sum + item.price, 0),
  [cartItems]
);
  1. Комбинирование состояний для UI:
const statusMessage = useMemo(() => {
  if (isLoading) return 'Загрузка...';
  if (error) return `Ошибка: ${error}`;
  return 'Готово';
}, [isLoading, error]);

Эти подходы делают интерфейс Haunted компонентов чётко синхронизированным с состоянием и повышают читаемость кода, разделяя данные и их производные представления.