121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
/**
|
||
* Оптимизированный хук для загрузки данных с кешированием
|
||
*/
|
||
|
||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||
import { cache } from '@/lib/cache';
|
||
import apiClient from '@/lib/api-client';
|
||
|
||
interface UseOptimizedFetchOptions<T> {
|
||
url: string;
|
||
cacheKey?: string;
|
||
/** Не используется пока кэш отключён глобально. Оставлен для последующего включения по месту. */
|
||
cacheTTL?: number;
|
||
enabled?: boolean;
|
||
onSuccess?: (data: T) => void;
|
||
onError?: (error: Error) => void;
|
||
}
|
||
|
||
interface UseOptimizedFetchResult<T> {
|
||
data: T | null;
|
||
loading: boolean;
|
||
error: Error | null;
|
||
refetch: () => Promise<void>;
|
||
clearCache: () => void;
|
||
mutate: (updater: T | ((prev: T | null) => T | null)) => void;
|
||
}
|
||
|
||
export function useOptimizedFetch<T = any>({
|
||
url,
|
||
cacheKey,
|
||
cacheTTL,
|
||
enabled = true,
|
||
onSuccess,
|
||
onError,
|
||
}: UseOptimizedFetchOptions<T>): UseOptimizedFetchResult<T> {
|
||
const key = cacheKey || url;
|
||
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 = useCallback(async () => {
|
||
// Отменяем предыдущий запрос
|
||
if (abortControllerRef.current) {
|
||
abortControllerRef.current.abort();
|
||
}
|
||
|
||
abortControllerRef.current = new AbortController();
|
||
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
// Кэш отключён везде до анализа на Production (см. lib/cache.ts, api-client.ts)
|
||
const response = await apiClient.get<T>(url, {
|
||
signal: abortControllerRef.current.signal,
|
||
cache: false,
|
||
});
|
||
|
||
const newData = response.data;
|
||
setData(newData);
|
||
onSuccess?.(newData);
|
||
} catch (err: any) {
|
||
// Не показываем ошибку при отмене запроса (навигация, размонтирование, refetch)
|
||
const isCanceled =
|
||
err?.name === 'AbortError' ||
|
||
err?.name === 'CanceledError' ||
|
||
err?.code === 'ERR_CANCELED' ||
|
||
err?.message === 'canceled';
|
||
if (isCanceled) {
|
||
return;
|
||
}
|
||
const error = err instanceof Error ? err : new Error(err?.message || 'Unknown error');
|
||
setError(error);
|
||
onError?.(error);
|
||
} finally {
|
||
if (abortControllerRef.current && !abortControllerRef.current.signal.aborted) {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
}, [url, onSuccess, onError]);
|
||
|
||
useEffect(() => {
|
||
if (!enabled) {
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
fetchData();
|
||
|
||
return () => {
|
||
if (abortControllerRef.current) {
|
||
abortControllerRef.current.abort();
|
||
}
|
||
};
|
||
}, [enabled, fetchData]);
|
||
|
||
const clearCache = useCallback(() => {
|
||
cache.delete(key);
|
||
setData(null);
|
||
}, [key]);
|
||
|
||
// Локальное обновление данных без перезагрузки (кэш отключён)
|
||
const mutate = useCallback((updater: T | ((prev: T | null) => T | null)) => {
|
||
setData((prev) => {
|
||
return typeof updater === 'function'
|
||
? (updater as (prev: T | null) => T | null)(prev)
|
||
: updater;
|
||
});
|
||
}, []);
|
||
|
||
return {
|
||
data,
|
||
loading,
|
||
error,
|
||
refetch: fetchData,
|
||
clearCache,
|
||
mutate,
|
||
};
|
||
}
|