← Назад к вопросам

Реализовать пользовательский хук useFetch

2.2 Middle🔥 111 комментариев
#Браузер и сетевые технологии

Условие

Создайте пользовательский React-хук useFetch для загрузки данных с API.

Требования

  1. Хук принимает URL для запроса
  2. Возвращает объект с полями:
    • data - полученные данные
    • loading - флаг загрузки
    • error - объект ошибки
  3. Поддержка отмены запроса при размонтировании
  4. Повторный запрос при изменении URL

Пример использования

function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(
    "https://api.example.com/users/" + userId
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{data.name}</div>;
}

Бонус

Добавьте функцию refetch для повторного запроса данных.

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Задача на создание хука useFetch — показывает понимание работы с API, управления состоянием и жизненным циклом компонента. Создадим несколько версий.

Решение 1: Базовый useFetch

Простая реализация с основной функциональностью:

import { useState, useEffect, useRef } from "react";

interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const abortControllerRef = useRef<AbortController | null>(null);

  useEffect(() => {
    // Создаём AbortController для отмены запроса
    abortControllerRef.current = new AbortController();

    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch(url, {
          signal: abortControllerRef.current?.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = (await response.json()) as T;
        setData(result);
      } catch (err) {
        // Игнорируем ошибку отмены (AbortError)
        if (err instanceof Error && err.name !== "AbortError") {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // Очистка - отменяем запрос при размонтировании
    return () => {
      abortControllerRef.current?.abort();
    };
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

Анализ:

  • AbortController — для отмены запроса при размонтировании
  • useState — для data, loading, error
  • useEffect — для инициирования запроса
  • Зависимость [url] — перезагружает при изменении URL
  • Обработка ошибок и AbortError

Использование базового хука

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useFetch<{ name: string; email: string }>(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data) return <div>No data</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}

Решение 2: С функцией refetch (бонус)

Расширенная версия с возможностью повторного запроса:

interface UseFetchWithRefetchResult<T> extends UseFetchResult<T> {
  refetch: () => Promise<void>;
}

function useFetchWithRefetch<T>(url: string): UseFetchWithRefetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const abortControllerRef = useRef<AbortController | null>(null);

  const fetchData = async (signal?: AbortSignal) => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(url, { signal });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result = (await response.json()) as T;
      setData(result);
    } catch (err) {
      if (err instanceof Error && err.name !== "AbortError") {
        setError(err);
      }
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    abortControllerRef.current = new AbortController();
    fetchData(abortControllerRef.current.signal);

    return () => {
      abortControllerRef.current?.abort();
    };
  }, [url]);

  // Функция для повторного запроса
  const refetch = async () => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    abortControllerRef.current = new AbortController();
    await fetchData(abortControllerRef.current.signal);
  };

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

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

function PostDetail({ postId }: { postId: string }) {
  const { data, loading, error, refetch } = useFetchWithRefetch<{
    title: string;
    body: string;
  }>(`https://jsonplaceholder.typicode.com/posts/${postId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{data?.title}</h1>
      <p>{data?.body}</p>
      <button onClick={refetch} className="px-4 py-2 bg-blue-500 text-white rounded">
        Refresh
      </button>
    </div>
  );
}

Решение 3: С опциональными параметрами

Более гибкая версия с настройками:

interface FetchOptions {
  method?: "GET" | "POST" | "PUT" | "DELETE";
  headers?: Record<string, string>;
  body?: any;
  cache?: "default" | "no-store" | "reload" | "force-cache";
  retryCount?: number;
  retryDelay?: number;
}

interface UseFetchOptionsResult<T> extends UseFetchWithRefetchResult<T> {
  reset: () => void;
}

function useFetchWithOptions<T>(
  url: string,
  options: FetchOptions = {}
): UseFetchOptionsResult<T> {
  const {
    method = "GET",
    headers = {},
    body,
    cache = "default",
    retryCount = 0,
    retryDelay = 1000,
  } = options;

  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const abortControllerRef = useRef<AbortController | null>(null);
  const retriesRef = useRef(0);

  const fetchData = async (signal?: AbortSignal, isRetry = false) => {
    if (!isRetry) {
      setLoading(true);
      setError(null);
      retriesRef.current = 0;
    }

    try {
      const fetchOptions: RequestInit = {
        method,
        headers: {
          "Content-Type": "application/json",
          ...headers,
        },
        cache,
        signal,
      };

      if (body) {
        fetchOptions.body = JSON.stringify(body);
      }

      const response = await fetch(url, fetchOptions);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result = (await response.json()) as T;
      setData(result);
    } catch (err) {
      if (err instanceof Error && err.name !== "AbortError") {
        // Повторный запрос при ошибке
        if (retriesRef.current < retryCount) {
          retriesRef.current++;
          setTimeout(() => {
            if (abortControllerRef.current?.signal.aborted === false) {
              fetchData(signal, true);
            }
          }, retryDelay);
        } else {
          setError(err);
        }
      }
    } finally {
      if (retriesRef.current >= retryCount) {
        setLoading(false);
      }
    }
  };

  useEffect(() => {
    abortControllerRef.current = new AbortController();
    fetchData(abortControllerRef.current.signal);

    return () => {
      abortControllerRef.current?.abort();
    };
  }, [url, JSON.stringify(options)]);

  const refetch = async () => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    abortControllerRef.current = new AbortController();
    await fetchData(abortControllerRef.current.signal);
  };

  const reset = () => {
    setData(null);
    setLoading(true);
    setError(null);
  };

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

Решение 4: С кешированием

Для избежания повторных запросов:

const cache = new Map<string, { data: any; timestamp: number }>();

function useFetchWithCache<T>(
  url: string,
  cacheTTL: number = 5 * 60 * 1000 // 5 минут
): UseFetchWithRefetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const cached = cache.get(url);
    const now = Date.now();

    // Если есть свежий кеш - используем его
    if (cached && now - cached.timestamp < cacheTTL) {
      setData(cached.data);
      setLoading(false);
      return;
    }

    let cancelled = false;
    const abortController = new AbortController();

    const fetchData = async () => {
      try {
        const response = await fetch(url, {
          signal: abortController.signal,
        });

        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        const result = (await response.json()) as T;

        // Сохраняем в кеш
        cache.set(url, { data: result, timestamp: Date.now() });

        if (!cancelled) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (!cancelled && err instanceof Error && err.name !== "AbortError") {
          setError(err);
        }
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchData();

    return () => {
      cancelled = true;
      abortController.abort();
    };
  }, [url, cacheTTL]);

  const refetch = async () => {
    cache.delete(url);
    setLoading(true);
  };

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

Сравнение подходов

ВариантПростотаФункцииRefetchКешРекомендация
БазовыйBasicНетНет✅ Best
С refetchGoodДаНет✅ Best
С опциями📊CompleteДаНетProduction
С кешем📊CompleteДаДаOptimization

Best Practices

  1. AbortController — для отмены запросов
  2. Обработка AbortError — отличать отмену от ошибок
  3. useRef — для хранения controller
  4. Зависимости — правильный [url]
  5. Типизация — generics для любых данных

Рекомендации для собеседования

  1. Начните с базового useFetch
  2. Объясните AbortController и отмену
  3. Добавьте refetch функцию (бонус)
  4. Покажите обработку ошибок
  5. Упомяните кеширование для оптимизации

Лучший выбор для production: Вариант 2 (с refetch) или Вариант 3 (с опциями) — полный функционал, гибкий, производительный.