refactor: add useDebounce hook and replace manual debounce in admin search

- Create reusable useDebounce<T> hook in frontend/src/hooks/useDebounce.ts
- Replace manual setTimeout/clearTimeout + extra state in AdminAppsPage
  with clean useDebounce(search, 400) pattern
- Removes timer state and handleSearch wrapper function
This commit is contained in:
keyhan
2026-04-08 12:40:25 +03:30
parent ecedf9119e
commit cb9320ee15
3 changed files with 33 additions and 12 deletions
+4 -11
View File
@@ -8,6 +8,7 @@ import { toast } from 'react-toastify';
import type { Application } from '@/types'; import type { Application } from '@/types';
import { Search, X, Package, Hexagon, User, Database, Box } from 'lucide-react'; import { Search, X, Package, Hexagon, User, Database, Box } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal'; import { useConfirm } from '@/components/confirm-modal';
import { useDebounce } from '@/hooks/useDebounce';
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
running: 'badge-green', running: 'badge-green',
@@ -23,15 +24,7 @@ export default function AdminAppsPage() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const confirm = useConfirm(); const confirm = useConfirm();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState(''); const debouncedSearch = useDebounce(search, 400);
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
const handleSearch = (value: string) => {
setSearch(value);
if (timer) clearTimeout(timer);
const t = setTimeout(() => setDebouncedSearch(value), 400);
setTimer(t);
};
const { data: apps = [], isLoading } = useQuery<Application[]>({ const { data: apps = [], isLoading } = useQuery<Application[]>({
queryKey: ['admin-applications', debouncedSearch], queryKey: ['admin-applications', debouncedSearch],
@@ -138,12 +131,12 @@ export default function AdminAppsPage() {
type="text" type="text"
placeholder="Search by app name, user name, email, or user ID…" placeholder="Search by app name, user name, email, or user ID…"
value={search} value={search}
onChange={(e) => handleSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
className="input pl-10 w-full" className="input pl-10 w-full"
/> />
{search && ( {search && (
<button <button
onClick={() => { setSearch(''); setDebouncedSearch(''); }} onClick={() => setSearch('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600" className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
+28
View File
@@ -0,0 +1,28 @@
'use client';
import { useState, useEffect } from 'react';
/**
* Debounce a value by a given delay.
*
* @param value The raw (fast-changing) value, e.g. a search input string.
* @param delay Debounce delay in milliseconds (default 400ms).
* @returns The debounced value only updates after the user stops
* changing `value` for `delay` ms.
*
* @example
* const [search, setSearch] = useState('');
* const debouncedSearch = useDebounce(search, 400);
*
* // use `debouncedSearch` in your query key / API call
*/
export function useDebounce<T>(value: T, delay = 400): T {
const [debounced, setDebounced] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
File diff suppressed because one or more lines are too long