← Назад к вопросам
Реализовать пользовательский хук useFetch
2.2 Middle🔥 111 комментариев
#Браузер и сетевые технологии
Условие
Создайте пользовательский React-хук useFetch для загрузки данных с API.
Требования
- Хук принимает URL для запроса
- Возвращает объект с полями:
- data - полученные данные
- loading - флаг загрузки
- error - объект ошибки
- Поддержка отмены запроса при размонтировании
- Повторный запрос при изменении 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, erroruseEffect— для инициирования запроса- Зависимость
[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 |
| С refetch | ✅ | Good | Да | Нет | ✅ Best |
| С опциями | 📊 | Complete | Да | Нет | Production |
| С кешем | 📊 | Complete | Да | Да | Optimization |
Best Practices
- AbortController — для отмены запросов
- Обработка AbortError — отличать отмену от ошибок
- useRef — для хранения controller
- Зависимости — правильный [url]
- Типизация — generics для любых данных
Рекомендации для собеседования
- Начните с базового useFetch
- Объясните AbortController и отмену
- Добавьте refetch функцию (бонус)
- Покажите обработку ошибок
- Упомяните кеширование для оптимизации
Лучший выбор для production: Вариант 2 (с refetch) или Вариант 3 (с опциями) — полный функционал, гибкий, производительный.