dressed_for_succes_store/frontend/hooks/useApiRequest.ts

272 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect, useCallback, useRef } from 'react';
import axios from 'axios'; // Убираем CancelTokenSource
import api, { apiStatus } from '@/lib/api';
interface UseApiRequestOptions<T> {
// URL для запроса
url: string;
// Метод запроса
method?: 'get' | 'post' | 'put' | 'delete';
// Параметры запроса
params?: any;
// Данные для отправки (для POST, PUT)
data?: any;
// Хедеры запроса
headers?: Record<string, string>;
// Автоматически выполнять запрос при монтировании
autoFetch?: boolean;
// Интервал для повторного запроса в миллисекундах
refreshInterval?: number;
// Количество автоматических повторных попыток при ошибке
retries?: number;
// Интервал между повторными попытками в миллисекундах
retryInterval?: number;
// Преобразователь для данных ответа
dataTransformer?: (data: any) => T;
// Функция для определения успешности ответа
isSuccessful?: (response: any) => boolean;
// Максимальное время запроса в миллисекундах
timeout?: number;
}
interface UseApiRequestResult<T> {
// Данные ответа
data: T | null;
// Состояние загрузки
loading: boolean;
// Ошибка, если есть
error: Error | null;
// Функция для выполнения запроса
fetchData: (config?: Partial<UseApiRequestOptions<T>>) => Promise<T | null>;
// Функция для отмены текущего запроса
cancelRequest: () => void;
// Функция для сброса состояния
reset: () => void;
// Статус запроса
status: 'idle' | 'loading' | 'success' | 'error';
}
// AbortController используется вместо CancelTokenSource
/**
* Хук для выполнения API-запросов с оптимизацией
* @param options Параметры запроса
* @returns Результат запроса и функции управления
*/
export function useApiRequest<T = any>(
options: UseApiRequestOptions<T>
): UseApiRequestResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
// Для хранения AbortController
const abortControllerRef = useRef<AbortController | null>(null);
// Для хранения таймера обновления
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
// Для отслеживания количества повторных попыток
const retriesCountRef = useRef<number>(0);
// Кэш последних успешных ответов по URL
const responseCache = useRef<Map<string, { data: T, timestamp: number }>>(new Map());
// Создаем ключ кэша на основе URL и параметров
const getCacheKey = useCallback((url: string, params?: any, data?: any): string => {
return `${url}${params ? `?${JSON.stringify(params)}` : ''}${data ? `|${JSON.stringify(data)}` : ''}`;
}, []);
// Функция для отмены текущего запроса
const cancelRequest = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort('Запрос отменен пользователем'); // Используем abort()
abortControllerRef.current = null;
}
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
refreshTimerRef.current = null;
}
}, []);
// Функция для сброса состояния
const reset = useCallback(() => {
cancelRequest();
setData(null);
setLoading(false);
setError(null);
setStatus('idle');
retriesCountRef.current = 0;
}, [cancelRequest]);
// Функция для создания источника отмены - больше не нужна, используем axios.CancelToken.source()
// Основная функция для выполнения запроса
const fetchData = useCallback(
async (overrideOptions?: Partial<UseApiRequestOptions<T>>): Promise<T | null> => {
try {
// Отменяем предыдущий запрос, если он есть
cancelRequest();
// Обновляем опции запроса
const currentOptions = { ...options, ...overrideOptions };
const {
url,
method = 'get',
params,
data: requestData,
headers,
retries = 0,
retryInterval = 1000,
dataTransformer,
isSuccessful = response => true,
timeout = 30000
} = currentOptions;
setLoading(true);
setStatus('loading');
setError(null);
// Проверяем наличие в кэше (только для GET запросов)
if (method === 'get') {
const cacheKey = getCacheKey(url || '', params || {}, null);
const cachedResponse = responseCache.current.get(cacheKey);
// Используем кэш, если он не старше 5 минут
if (cachedResponse && Date.now() - cachedResponse.timestamp < 5 * 60 * 1000) {
console.log(`Используются кэшированные данные для ${url}`);
setData(cachedResponse.data);
setLoading(false);
setStatus('success');
return cachedResponse.data;
}
}
// Создаем новый AbortController
abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal;
// Выполняем запрос с помощью api-клиента, передавая signal
let response;
try {
const config = { headers, timeout, signal }; // Добавляем signal в конфиг
switch (method) {
case 'get':
response = await api.get(url, { params: params || {}, ...config }); // Передаем config
break;
case 'post':
response = await api.post(url, requestData || {}, config); // Передаем config
break;
case 'put':
response = await api.put(url, requestData || {}, config); // Передаем config
break;
case 'delete':
response = await api.delete(url, config); // Передаем config
break;
default:
throw new Error(`Неподдерживаемый метод: ${method}`);
}
} catch (error) {
throw error;
}
// Проверяем успешность ответа
if (!isSuccessful(response)) {
throw new Error('Ответ API не соответствует ожидаемому формату');
}
// Преобразуем данные, если указан трансформер
const processedData = dataTransformer ? dataTransformer(response) : (response as T);
// Кэшируем ответ для GET запросов
if (method === 'get') {
const cacheKey = getCacheKey(url || '', params || {}, null);
responseCache.current.set(cacheKey, {
data: processedData,
timestamp: Date.now()
});
// Ограничиваем размер кэша
if (responseCache.current.size > 50) {
const oldestKey = responseCache.current.keys().next().value;
responseCache.current.delete(oldestKey);
}
}
// Обновляем состояние с полученными данными
setData(processedData);
setLoading(false);
setStatus('success');
retriesCountRef.current = 0;
// Устанавливаем таймер для автоматического обновления, если задан
if (currentOptions.refreshInterval && currentOptions.refreshInterval > 0) {
refreshTimerRef.current = setTimeout(() => {
fetchData(overrideOptions);
}, currentOptions.refreshInterval);
}
return processedData;
} catch (err: unknown) { // Явно типизируем err как unknown
// Обработка ошибки отмены (AbortError)
if (err instanceof Error && err.name === 'AbortError') {
// Используем сообщение из AbortSignal, если оно есть, иначе стандартное
const abortReason = (abortControllerRef.current?.signal.reason as string) || 'Запрос отменен';
console.log('Запрос отменен:', abortReason);
setLoading(false);
setStatus('idle');
return null;
}
// Обработка других ошибок
console.error('Ошибка API-запроса:', err);
const errorObj = err instanceof Error ? err : new Error('Неизвестная ошибка при запросе к API');
setError(errorObj);
setLoading(false);
setStatus('error');
// Повторная попытка, если указано количество повторов
const { retries = 0, retryInterval = 1000 } = { ...options, ...overrideOptions };
if (retriesCountRef.current < retries) {
console.log(`Повторная попытка ${retriesCountRef.current + 1}/${retries} через ${retryInterval}мс`);
retriesCountRef.current++;
await new Promise(resolve => setTimeout(resolve, retryInterval));
return fetchData(overrideOptions);
}
return null;
}
},
// Убираем createCancelToken из зависимостей
[options, cancelRequest, getCacheKey]
);
// Автоматический запрос при монтировании компонента
useEffect(() => {
if (options.autoFetch) {
fetchData();
}
// Очистка
return () => {
cancelRequest();
};
}, [fetchData, cancelRequest, options.autoFetch]);
return {
data,
loading,
error,
fetchData,
cancelRequest,
reset,
status
};
}