Как сделать чтобы запрос не строился столько же раз сколько нажата кнопка получения отчета?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как сделать чтобы запрос не строился столько же раз сколько нажата кнопка
Это проблема, когда пользователь многократно нажимает на кнопку "Получить отчет" и каждый клик отправляет новый запрос на сервер. Нужно предотвратить множественные одновременные запросы. Вот несколько проверенных решений.
Проблема: Множественные запросы
// ПЛОХО - каждый клик отправляет запрос
function ReportGenerator() {
const handleGenerateReport = async () => {
// Пользователь может нажать 5 раз за 0.5 сек
// 5 запросов уйдут на сервер!
const response = await fetch('/api/generate-report');
const data = await response.json();
console.log(data);
};
return <button onClick={handleGenerateReport}>Get Report</button>;
}
Решение 1: Отключение кнопки во время запроса
Это самое простое и эффективное решение.
function ReportGenerator() {
const [isLoading, setIsLoading] = useState(false);
const handleGenerateReport = async () => {
if (isLoading) return; // Игнорируем повторные клики
setIsLoading(true);
try {
const response = await fetch('/api/generate-report');
const data = await response.json();
console.log('Report ready:', data);
} catch (error) {
console.error('Error generating report:', error);
} finally {
setIsLoading(false);
}
};
return (
<button
onClick={handleGenerateReport}
disabled={isLoading}
className={isLoading ? 'opacity-50 cursor-not-allowed' : ''}
>
{isLoading ? 'Generating...' : 'Get Report'}
</button>
);
}
Решение 2: Дебоунсирование с useCallback
Дебоунс откладывает выполнение функции до тех пор, пока пользователь не закончит кликать.
function useDebounce<T extends (...args: any[]) => void>(
callback: T,
delay: number = 500
): T {
const timeoutRef = useRef<NodeJS.Timeout>();
return ((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}) as T;
}
function ReportGenerator() {
const handleGenerateReport = useDebounce(async () => {
const response = await fetch('/api/generate-report');
const data = await response.json();
console.log('Report:', data);
}, 800);
return <button onClick={handleGenerateReport}>Get Report</button>;
}
Решение 3: Throttling (Дросселирование)
Тротлинг обеспечивает максимум один запрос в N миллисекунд.
function useThrottle<T extends (...args: any[]) => void>(
callback: T,
delay: number = 1000
): T {
const lastRunRef = useRef(Date.now());
const timeoutRef = useRef<NodeJS.Timeout>();
return ((...args) => {
const now = Date.now();
const timeSinceLastRun = now - lastRunRef.current;
if (timeSinceLastRun >= delay) {
// Прошло достаточно времени - выполняем сразу
callback(...args);
lastRunRef.current = now;
} else {
// Планируем выполнение
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
callback(...args);
lastRunRef.current = Date.now();
}, delay - timeSinceLastRun);
}
}) as T;
}
function ReportGenerator() {
const handleGenerateReport = useThrottle(async () => {
const response = await fetch('/api/generate-report');
const data = await response.json();
console.log('Report:', data);
}, 2000); // Максимум один запрос в 2 секунды
return <button onClick={handleGenerateReport}>Get Report</button>;
}
Решение 4: AbortController для отмены предыдущего запроса
Если новый запрос отправляется до завершения предыдущего, отменяем старый.
function ReportGenerator() {
const [isLoading, setIsLoading] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const handleGenerateReport = async () => {
// Отменяем предыдущий запрос если он еще выполняется
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Создаем новый AbortController
abortControllerRef.current = new AbortController();
setIsLoading(true);
try {
const response = await fetch('/api/generate-report', {
signal: abortControllerRef.current.signal,
});
const data = await response.json();
console.log('Report:', data);
} catch (error: any) {
if (error.name !== 'AbortError') {
console.error('Error:', error);
}
} finally {
setIsLoading(false);
}
};
return (
<button
onClick={handleGenerateReport}
disabled={isLoading}
>
{isLoading ? 'Generating...' : 'Get Report'}
</button>
);
}
Решение 5: Custom Hook с комбинацией подходов
Комбинируем несколько техник для надежного решения.
interface UseAsyncActionOptions {
debounce?: number;
throttle?: number;
timeout?: number; // Таймаут запроса
}
function useAsyncAction<T>(
asyncFn: () => Promise<T>,
options: UseAsyncActionOptions = {}
) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [data, setData] = useState<T | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const lastRunRef = useRef(0);
const isMountedRef = useRef(true);
// Cleanup на unmount
useEffect(() => {
return () => {
isMountedRef.current = false;
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
const execute = useCallback(async () => {
// Throttle проверка
if (options.throttle) {
const now = Date.now();
if (now - lastRunRef.current < options.throttle) {
return;
}
lastRunRef.current = now;
}
// Отменяем предыдущий запрос
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
setIsLoading(true);
setError(null);
try {
// Обертка для отмены по таймауту
const promise = asyncFn();
if (options.timeout && !options.timeout) {
timeoutRef.current = setTimeout(
() => abortControllerRef.current?.abort(),
options.timeout
);
}
const result = await promise;
if (isMountedRef.current) {
setData(result);
}
} catch (err) {
if (isMountedRef.current && (err as any).name !== 'AbortError') {
setError(err as Error);
}
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
}
}, [asyncFn, options]);
// Debounce обертка
const debouncedExecute = useCallback(() => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (options.debounce) {
debounceTimerRef.current = setTimeout(execute, options.debounce);
} else {
execute();
}
}, [execute, options.debounce]);
return {
isLoading,
error,
data,
execute: debouncedExecute,
};
}
// Использование
function ReportGenerator() {
const { isLoading, error, data, execute } = useAsyncAction(
async () => {
const response = await fetch('/api/generate-report');
if (!response.ok) throw new Error('Failed to generate report');
return response.json();
},
{
debounce: 500, // Дебоунс 500мс
throttle: 2000, // Минимум 2сек между запросами
timeout: 30000, // Таймаут 30сек
}
);
return (
<div>
<button onClick={execute} disabled={isLoading}>
{isLoading ? 'Generating...' : 'Get Report'}
</button>
{error && <p className="text-red-600">Error: {error.message}</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
Решение 6: React Query (рекомендуется для production)
React Query автоматически обрабатывает дублирующиеся запросы.
import { useMutation } from '@tanstack/react-query';
function ReportGenerator() {
const mutation = useMutation({
mutationFn: async () => {
const response = await fetch('/api/generate-report');
return response.json();
},
});
return (
<div>
<button
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Generating...' : 'Get Report'}
</button>
{mutation.error && <p>Error: {mutation.error.message}</p>}
{mutation.data && <pre>{JSON.stringify(mutation.data, null, 2)}</pre>}
</div>
);
}
Сравнение подходов
const comparison = {
'Отключение кнопки': {
сложность: 'Простая',
эффективность: 'Высокая',
UX: 'Хороший',
использование: 'Рекомендуется для простых случаев',
},
'Дебоунс': {
сложность: 'Средняя',
эффективность: 'Средняя',
UX: 'Может быть задержка',
использование: 'Для форм и поиска',
},
'Тротлинг': {
сложность: 'Средняя',
эффективность: 'Высокая',
UX: 'Хороший, но может быть задержка',
использование: 'Для частых операций',
},
'AbortController': {
сложность: 'Средняя',
эффективность: 'Высокая',
UX: 'Отличный',
использование: 'Когда нужно отменить старый запрос',
},
'React Query': {
сложность: 'Низкая (если уже используется)',
эффективность: 'Очень высокая',
UX: 'Отличный',
использование: 'Рекомендуется для production',
},
};
Лучшие практики
- Всегда отключай кнопку во время загрузки
- Используй AbortController для отмены старых запросов
- Добавляй дебоунс для поиска и фильтров
- Рассмотри React Query для более сложных сценариев
- Показывай пользователю статус (loading, success, error)
- Логируй попытки отправить дублирующиеся запросы
- Установи таймауты на запросы
- Тестируй быстрые клики и плохое соединение
Выбор подхода зависит от сложности приложения и требований. Для большинства случаев комбинация отключения кнопки + AbortController - это оптимальное решение.