Типичные ошибки и их решения

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


Некорректное использование useState и асинхронного кода

Проблема: Часто возникает ошибка, когда состояние обновляется асинхронно, но компонент уже размонтирован. Например:

import { component, useState } from 'haunted';

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

  setTimeout(() => {
    setSeconds(seconds + 1);
  }, 1000);

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

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

Почему это ошибка:

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

Решение: Использовать useEffect для управления побочными эффектами и очистки ресурсов:

import { 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>${seconds}</p>`;
}

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

Неправильное использование useEffect с зависимостями

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

useEffect(() => {
  fetchData().then(setData);
}, []); // пустой массив зависимостей

Почему это ошибка:

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

Решение: Тщательно определять зависимости:

useEffect(() => {
  fetchData(userId).then(setData);
}, [userId]); // перезапуск при изменении userId

Неверная работа с useRef

Проблема: Попытка напрямую изменять DOM до первого рендера или через обычную переменную:

let inputEl;

function MyComponent() {
  return html`<input ${ref(el => inputEl = el)} />`;
}

Почему это ошибка:

  • Простые переменные не сохраняют значение между ререндерингами.
  • Попытка доступа к DOM до рендера может вернуть undefined.

Решение: Использовать useRef из Haunted:

import { useRef } from 'haunted';

function MyComponent() {
  const inputRef = useRef();

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return html`<input ${ref(inputRef)} />`;
}

Ошибки при передаче пропсов в компонент

Проблема: Неявное сравнение объектов или массивов приводит к бесконечным ререндерингам:

function List({ items }) {
  useEffect(() => {
    console.log('Items changed');
  }, [items]); // items может быть новым объектом при каждом рендере
}

Почему это ошибка:

  • Каждый ререндер родителя создаёт новый массив items. Эффект срабатывает даже если содержимое не изменилось.

Решение:

  • Использовать мемоизацию пропсов через useMemo или константные ссылки:
const itemsMemo = useMemo(() => items, [items.length]);
<List items={itemsMemo} />

Несоблюдение правил работы с html шаблонами

Проблема: Попытка вставить JavaScript-объект или массив напрямую в шаблон:

return html`<p>${[1,2,3]}</p>`;

Почему это ошибка:

  • Haunted рендерит массив как текст, а не как отдельные элементы.
  • Обновление элементов массива может привести к лишним перерендерингам.

Решение: Использовать map для создания элементов:

return html`<ul>
  ${[1,2,3].map(n => html`<li>${n}</li>`)}
</ul>`;

Утечки памяти из-за незакрытых подписок

Проблема: Подписки на события или WebSocket остаются активными после размонтирования:

useEffect(() => {
  socket.on('message', handleMessage);
}, []); // отсутствие очистки

Почему это ошибка:

  • События продолжают слушаться после удаления компонента.
  • Это может приводить к увеличению памяти и неконтролируемым вызовам.

Решение: Очистка подписок в return эффекта:

useEffect(() => {
  socket.on('message', handleMessage);
  return () => socket.off('message', handleMessage);
}, []);

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

Проблема: Инициализация состояния на каждом рендере приводит к сбросу значений:

const [count, setCount] = useState(Math.random());

Почему это ошибка:

  • Каждый ререндер заново вызывает Math.random(), что сбрасывает состояние.

Решение:

  • Использовать ленивую инициализацию:
const [count, setCount] = useState(() => Math.random());

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