Обработка промисов в компонентах

Preact предоставляет легковесную альтернативу React с почти идентичным API, включая поддержку функциональных и классовых компонентов. Одной из ключевых задач при разработке интерфейсов является асинхронная обработка данных, чаще всего с использованием промисов. Понимание того, как корректно интегрировать промисы в компоненты, является основой для построения отзывчивых и эффективных приложений.


Асинхронные операции в функциональных компонентах

Функциональные компоненты в Preact используют хуки для управления состоянием и побочными эффектами. Основным инструментом для работы с промисами является хук useEffect совместно с useState.

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

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

    useEffect(() => {
        setLoading(true);
        fetch(url)
            .then(response => {
                if (!response.ok) throw new Error('Ошибка загрузки данных');
                return response.json();
            })
            .then(json => setData(json))
            .catch(err => setError(err))
            .finally(() => setLoading(false));
    }, [url]);

    if (loading) return <p>Загрузка...</p>;
    if (error) return <p>Ошибка: {error.message}</p>;
    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

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

  • useEffect используется для запуска асинхронного кода после рендера компонента.
  • Зависимости [url] гарантируют повторный вызов эффекта при изменении адреса запроса.
  • finally позволяет завершить загрузку независимо от результата промиса.
  • Обработка ошибок через .catch предотвращает падение компонента.

Асинхронные функции внутри useEffect

Вызов асинхронных функций напрямую в useEffect невозможен, так как эффект не может возвращать промис. Вместо этого создается внутренняя асинхронная функция:

useEffect(() => {
    const fetchData = async () => {
        try {
            setLoading(true);
            const response = await fetch(url);
            if (!response.ok) throw new Error('Ошибка загрузки данных');
            const result = await response.json();
            setData(result);
        } catch (err) {
            setError(err);
        } finally {
            setLoading(false);
        }
    };

    fetchData();
}, [url]);

Преимущества такого подхода:

  • Код становится более читаемым благодаря синтаксису async/await.
  • Исключения обрабатываются через try/catch.
  • Управление состояниями loading, data и error централизовано.

Отмена асинхронных запросов

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

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

    const fetchData = async () => {
        try {
            const response = await fetch(url);
            const result = await response.json();
            if (!isCancelled) setData(result);
        } catch (err) {
            if (!isCancelled) setError(err);
        }
    };

    fetchData();

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

Флаг isCancelled предотвращает вызов setState после размонтирования, что исключает появление предупреждений типа “Can’t perform a React state update on an unmounted component”.


Асинхронные операции в классовых компонентах

Классовые компоненты в Preact также поддерживают работу с промисами через методы жизненного цикла. Наиболее часто используется componentDidMount:

import { h, Component } from 'preact';

class DataFetcherClass extends Component {
    state = {
        data: null,
        loading: true,
        error: null
    };

    componentDidMount() {
        fetch(this.props.url)
            .then(response => {
                if (!response.ok) throw new Error('Ошибка загрузки');
                return response.json();
            })
            .then(data => this.setState({ data }))
            .catch(error => this.setState({ error }))
            .finally(() => this.setState({ loading: false }));
    }

    render() {
        const { data, loading, error } = this.state;

        if (loading) return <p>Загрузка...</p>;
        if (error) return <p>Ошибка: {error.message}</p>;
        return <pre>{JSON.stringify(data, null, 2)}</pre>;
    }
}

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

  • В классовых компонентах состояние управляется через this.setState.
  • Методы жизненного цикла позволяют запускать промисы после монтирования или перед размонтированием для очистки ресурсов.
  • Отмена промисов возможна через флаги или специализированные библиотеки вроде AbortController.

Использование AbortController для отмены запросов

AbortController позволяет явно прерывать асинхронные запросы, предотвращая лишние обновления состояния:

useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
        try {
            const response = await fetch(url, { signal: controller.signal });
            if (!response.ok) throw new Error('Ошибка загрузки');
            const result = await response.json();
            setData(result);
        } catch (err) {
            if (err.name !== 'AbortError') setError(err);
        } finally {
            setLoading(false);
        }
    };

    fetchData();

    return () => controller.abort();
}, [url]);

Преимущества использования AbortController:

  • Явная отмена HTTP-запросов.
  • Исключение состояния гонки при быстром смене пропсов.
  • Чистый и безопасный способ предотвращения ошибок при размонтировании компонента.

Комбинирование нескольких промисов

Для работы с несколькими асинхронными операциями одновременно можно использовать Promise.all или Promise.allSettled:

useEffect(() => {
    const fetchMultiple = async () => {
        try {
            setLoading(true);
            const [usersResponse, postsResponse] = await Promise.all([
                fetch('/api/users'),
                fetch('/api/posts')
            ]);
            const users = await usersResponse.json();
            const posts = await postsResponse.json();
            setData({ users, posts });
        } catch (err) {
            setError(err);
        } finally {
            setLoading(false);
        }
    };

    fetchMultiple();
}, []);
  • Promise.all завершится с ошибкой, если хотя бы один промис не выполнен.
  • Promise.allSettled позволяет обработать все результаты независимо от ошибок.
  • Этот подход упрощает синхронизацию нескольких асинхронных источников данных.

Вывод

Правильная обработка промисов в Preact требует понимания особенностей хуков и жизненного цикла компонентов. Использование useEffect с асинхронными функциями, флагов отмены или AbortController позволяет создавать надежные компоненты, которые корректно обрабатывают асинхронные операции и избегают проблем с состоянием. Комбинация этих методов обеспечивает как чистоту кода, так и устойчивость интерфейса к быстрым изменениям данных.