Кастомный хук для Fetch

React-хук useFetch — загружает данные с API, управляет состоянием loading/error/data и отменяет запрос при размонтировании.

Задача

В React-компонентах нужно загружать данные и управлять состояниями загрузки, ошибки и данных. Дублировать useEffect + fetch в каждом компоненте неудобно — нужен переиспользуемый хук.

Решение

// hooks/useFetch.ts
import { useState, useEffect, useCallback } from 'react';

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch:  => void;
}

function useFetch<T>(url: string, options?: RequestInit): FetchState<T> {
  const [data, setData]       = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError]     = useState<Error | null>(null);
  const [tick, setTick]       = useState(0); // триггер для refetch

  const refetch = useCallback( => setTick((n) => n + 1), );

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(null);

    fetch(url, { ...options, signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then((json) => {
        setData(json);
        setLoading(false);
      })
      .catch((err: Error) => {
        if (err.name === 'AbortError') return; // компонент размонтирован
        setError(err);
        setLoading(false);
      });

    return  => controller.abort();
  }, [url, tick]); // eslint-disable-line react-hooks/exhaustive-deps

  return { data, loading, error, refetch };
}

export default useFetch;

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

import useFetch from '@/hooks/useFetch';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const { data: user, loading, error, refetch } = useFetch<User>(`/api/users/${userId}`);

  if (loading) return <p>Загрузка...</p>;
  if (error)   return <p>Ошибка: {error.message} <button onClick={refetch}>Повторить</button></p>;
  if (!user)   return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

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

  • AbortController — отменяет запрос при размонтировании компонента; предотвращает setState на размонтированный компонент.
  • tick в зависимостях useEffect — позволяет вызвать refetch без изменения url.
  • err.name === 'AbortError' — AbortController бросает ошибку при отмене; её нужно игнорировать.
  • Хук возвращает refetch — удобно для кнопки «Повторить» при ошибке.

Варианты

  • SWR (useSWR) — кэширование, revalidation, optimistic updates — для продакшн-приложений.
  • TanStack Query (useQuery) — мощнее SWR, поддерживает мутации, пагинацию, infinite scroll.
  • React 19 use — нативный способ загрузки данных через Suspense.

Связанные рецепты / темы