Async/await в хуках

Использование асинхронного кода в Preact требует аккуратного подхода, особенно внутри функциональных компонентов и хуков. Несмотря на то, что Preact совместим с React-подходами, важно учитывать лёгкость и минимализм Preact, чтобы не создавать лишние перерендеры или утечки памяти.


Асинхронные операции в useEffect

Хук useEffect является основным инструментом для работы с побочными эффектами, включая асинхронные запросы. Прямое использование async функции в useEffect невозможно, так как сам эффект должен возвращать либо undefined, либо функцию очистки:

import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';

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

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

    async function fetchData() {
      try {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        if (isMounted) {
          setData(result);
        }
      } catch (error) {
        console.error(error);
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }

    fetchData();

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

  return loading ? <p>Загрузка...</p> : <pre>{JSON.stringify(data, null, 2)}</pre>;
}

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

  • Использование внутренней асинхронной функции внутри useEffect.
  • Переменная isMounted предотвращает обновление состояния после размонтирования компонента.
  • Обработка ошибок с try/catch обеспечивает стабильность работы при сетевых сбоях.

Асинхронные действия в пользовательских хуках

Создание собственных хуков для асинхронной логики повышает повторное использование кода. Важный аспект — корректное управление состоянием загрузки и результатами:

import { useState, useEffect } from 'preact/hooks';

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

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

    async function execute() {
      setLoading(true);
      setError(null);
      try {
        const result = await fetchFunction();
        if (isMounted) setData(result);
      } catch (err) {
        if (isMounted) setError(err);
      } finally {
        if (isMounted) setLoading(false);
      }
    }

    execute();

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

  return { data, loading, error };
}

Применение хука:

function App() {
  const { data, loading, error } = useAsync(() => fetch('https://api.example.com/items').then(res => res.json()));

  if (loading) return <p>Загрузка...</p>;
  if (error) return <p>Ошибка: {error.message}</p>;

  return <ul>{data.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

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

  • Хук принимает функцию запроса, что делает его универсальным.
  • Контроль isMounted предотвращает утечки памяти и ошибки при обновлении состояния после размонтирования.

Асинхронные обработчики событий

Асинхронные функции можно использовать в обработчиках событий, например, onClick. Важно правильно обрабатывать состояние и ошибки:

function LoadButton() {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState(null);

  async function handleClick() {
    setLoading(true);
    try {
      const response = await fetch('https://api.example.com/button-data');
      const result = await response.json();
      setData(result);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <button onCl ick={handleClick} disabled={loading}>
        {loading ? 'Загрузка...' : 'Загрузить данные'}
      </button>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  );
}

Замечания:

  • async функции в событиях не вызывают побочных эффектов Preact, кроме как обычного обновления состояния.
  • Контроль loading позволяет блокировать повторные нажатия и улучшает UX.

Паттерны обработки ошибок

Асинхронные хуки и обработчики требуют надежного механизма обработки ошибок. Рекомендуется комбинировать:

  1. try/catch для синхронной и асинхронной логики.
  2. Внутреннее состояние error для отображения ошибок в UI.
  3. Переменные контроля монтирования компонента (isMounted) для предотвращения утечек.
useEffect(() => {
  let isMounted = true;
  async function load() {
    try {
      const result = await fetchData();
      if (isMounted) setData(result);
    } catch (err) {
      if (isMounted) setError(err);
    }
  }
  load();
  return () => { isMounted = false; };
}, []);

Параллельные и последовательные асинхронные вызовы

Preact позволяет организовать несколько асинхронных вызовов внутри эффекта:

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

  async function loadAll() {
    try {
      const [users, posts] = await Promise.all([
        fetch('/users').then(res => res.json()),
        fetch('/posts').then(res => res.json())
      ]);
      if (isMounted) {
        setUsers(users);
        setPosts(posts);
      }
    } catch (error) {
      console.error(error);
    }
  }

  loadAll();

  return () => { isMounted = false; };
}, []);
  • Promise.all позволяет выполнять запросы параллельно, ускоряя загрузку данных.
  • Для последовательных операций достаточно обычного await в цепочке вызовов.

Рекомендации по оптимизации

  • Минимизировать количество асинхронных эффектов, объединяя логически связанные операции.
  • Использовать мемоизацию функций запроса с useCallback, чтобы избежать лишних перезапусков useEffect.
  • Контролировать состояние загрузки и ошибки для предсказуемого UI.
  • Избегать прямого async в useEffect, всегда оборачивая в внутреннюю функцию.

Асинхронные операции в Preact требуют аккуратного управления состоянием и размонтированием компонентов. Корректное применение async/await в сочетании с хуками позволяет создавать эффективные, стабильные и легко поддерживаемые приложения.