Загрузка данных в useEffect

Библиотека Haunted предоставляет функциональный подход к созданию веб-компонентов с использованием реактивных хуков, аналогичных React. Одним из ключевых аспектов является управление побочными эффектами и асинхронными операциями, такими как загрузка данных с сервера. Для этого используется хук useEffect, который позволяет выполнять действия после рендеринга компонента и контролировать их повторное выполнение.


Основы useEffect

useEffect принимает два аргумента:

useEffect(() => {
  // эффект
  return () => {
    // очистка эффекта
  };
}, [зависимости]);
  • Первый аргумент — функция, выполняющая побочный эффект.
  • Возвращаемая функция используется для очистки эффекта (аналог componentWillUnmount).
  • Второй аргумент — массив зависимостей. Эффект будет выполняться только при изменении этих зависимостей. Если массив пустой ([]), эффект выполнится один раз после первого рендера.

Асинхронная загрузка данных

Прямое объявление асинхронной функции внутри useEffect не поддерживается:

useEffect(async () => {
  const data = await fetchData(); // так делать нельзя
}, []);

Правильный способ — определить асинхронную функцию внутри эффекта и вызвать её:

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

function DataLoader() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/items');
        if (!response.ok) throw new Error('Ошибка сети');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

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

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

  • Создание асинхронной функции внутри useEffect позволяет использовать await.
  • Состояние loading помогает отслеживать процесс загрузки.
  • Обработка ошибок через try/catch гарантирует стабильность компонента.

Зависимости и повторная загрузка

Массив зависимостей контролирует повторный вызов эффекта. Если загрузка данных зависит от каких-либо пропсов или состояния, эти значения необходимо указать в массиве:

function UserLoader({ userId }) {
  const [user, setUser] = useState(null);

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

    const fetchUser = async () => {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const data = await response.json();
      if (!cancelled) setUser(data);
    };

    fetchUser();

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

  return html`${user ? html`<p>${user.name}</p>` : html`<p>Загрузка...</p>`}`;
}
  • Эффект выполняется при изменении userId.
  • Использование флага cancelled предотвращает обновление состояния после размонтирования компонента.

Очистка эффектов и предотвращение утечек памяти

При работе с асинхронными запросами важно избегать обновления состояния на размонтированном компоненте. Для этого внутри эффекта возвращается функция очистки:

useEffect(() => {
  let isMounted = true;

  fetch('https://api.example.com/items')
    .then(res => res.json())
    .then(data => {
      if (isMounted) setData(data);
    });

  return () => { isMounted = false; };
}, []);

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


Сложные сценарии: несколько эффектов

Можно использовать несколько useEffect для разделения логики:

useEffect(() => {
  // загрузка данных
}, []);

useEffect(() => {
  // подписка на события
  return () => { /* отписка */ };
}, []);

Разделение эффектов повышает читаемость и управляемость кода. Каждый эффект отвечает за отдельную задачу: один за асинхронную загрузку, другой — за события или таймеры.


Использование useEffect с кастомными хуками

Для повторного использования логики загрузки данных удобно создавать кастомные хуки:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

    const fetchData = async () => {
      try {
        const res = await fetch(url);
        const json = await res.json();
        if (!cancelled) setData(json);
      } catch (err) {
        if (!cancelled) setError(err);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchData();

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

  return { data, loading, error };
}

Использование:

function ItemsList() {
  const { data, loading, error } = useFetch('https://api.example.com/items');

  return html`
    ${loading ? html`<p>Загрузка...</p>` :
      error ? html`<p>Ошибка: ${error.message}</p>` :
      html`<ul>${data.map(item => html`<li>${item.name}</li>`)}</ul>`}
  `;
}
  • Кастомный хук инкапсулирует всю логику загрузки и обработки ошибок.
  • Компоненты становятся проще и сосредоточены на отображении данных.

Резюме подходов

  1. Асинхронные операции выполняются через внутренние функции в useEffect.
  2. Контроль зависимостей предотвращает ненужные повторные вызовы.
  3. Очистка эффектов предотвращает утечки памяти и ошибки при размонтировании.
  4. Разделение эффектов и использование кастомных хуков повышает читаемость и переиспользуемость кода.
  5. Состояния loading, error и data формируют устойчивый паттерн для всех асинхронных операций в Haunted.

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