From 86418ac5e22bc026b6c6df93ff57ec5282e5ae02 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 21 May 2026 10:21:14 +0530 Subject: [PATCH 1/2] fix: optimize GitHub search with debounce and request cancellation --- src/hooks/useDebounce.ts | 25 ++++++++ src/hooks/useGitHubData.ts | 89 ++++++++++++++++++++------ src/pages/Tracker/Tracker.tsx | 117 +++++++++++++++++----------------- 3 files changed, 152 insertions(+), 79 deletions(-) create mode 100644 src/hooks/useDebounce.ts diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 00000000..f14ff056 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +/** + * Custom hook for debouncing values + * Delays updating a state value until after the user stops changing it for a specified duration + * + * @param value - The value to debounce + * @param delay - The debounce delay in milliseconds (default: 300ms) + * @returns The debounced value + */ +export const useDebounce = (value: T, delay: number = 300): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + // Set up the timeout + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + // Clean up the timeout if value changes before delay is complete + return () => clearTimeout(handler); + }, [value, delay]); + + return debouncedValue; +}; diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index 68a8a0cd..556e3168 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useRef, useEffect } from 'react'; export const useGitHubData = (getOctokit: () => any) => { const [issues, setIssues] = useState([]); @@ -9,7 +9,32 @@ export const useGitHubData = (getOctokit: () => any) => { const [totalPrs, setTotalPrs] = useState(0); const [rateLimited, setRateLimited] = useState(false); - const fetchPaginated = async (octokit: any, username: string, type: string, page = 1, per_page = 10) => { + // Store AbortController to cancel in-flight requests + const abortControllerRef = useRef(null); + + // Cleanup function to cancel any pending requests + const cancelPendingRequest = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + }, []); + + // Cleanup on component unmount + useEffect(() => { + return () => { + cancelPendingRequest(); + }; + }, [cancelPendingRequest]); + + const fetchPaginated = async ( + octokit: any, + username: string, + type: string, + page = 1, + per_page = 10, + signal?: AbortSignal + ) => { const q = `author:${username} is:${type}`; const response = await octokit.request('GET /search/issues', { q, @@ -17,6 +42,9 @@ export const useGitHubData = (getOctokit: () => any) => { order: 'desc', per_page, page, + request: { + signal, // Pass AbortSignal to the request + }, }); return { @@ -27,41 +55,62 @@ export const useGitHubData = (getOctokit: () => any) => { const fetchData = useCallback( async (username: string, page = 1, perPage = 10) => { - + // Validate inputs + if (!username || username.trim().length === 0) { + setError('Please enter a GitHub username.'); + return; + } + const octokit = getOctokit(); - if (!octokit || !username) return; + if (!octokit) { + setError('Authentication not initialized.'); + return; + } + + // Cancel any existing in-flight requests + cancelPendingRequest(); + + // Create new AbortController for this request + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; setLoading(true); setError(''); try { const [issueRes, prRes] = await Promise.all([ - fetchPaginated(octokit, username, 'issue', page, perPage), - fetchPaginated(octokit, username, 'pr', page, perPage), + fetchPaginated(octokit, username, 'issue', page, perPage, signal), + fetchPaginated(octokit, username, 'pr', page, perPage, signal), ]); - setIssues(issueRes.items); - setPrs(prRes.items); - setTotalIssues(issueRes.total); - setTotalPrs(prRes.total); - setRateLimited(false); + // Check if request was aborted before updating state + if (!signal.aborted) { + setIssues(issueRes.items); + setPrs(prRes.items); + setTotalIssues(issueRes.total); + setTotalPrs(prRes.total); + setRateLimited(false); + } } catch (err: any) { + // Don't show error if request was intentionally aborted + if (err.name === 'AbortError') { + return; + } + const errorMessage = err.message?.toLowerCase() || ""; if (err.status === 403) { setError('GitHub API rate limit exceeded. Please provide a PAT to continue.'); - setRateLimited(true); - } else if (errorMessage.includes("do not exist")){ + setRateLimited(true); + } else if (errorMessage.includes("do not exist")) { setError('User not found. Please check the spelling of the GitHub username.'); - } else if (err.status === 401 || errorMessage.includes("permission")){ + } else if (err.status === 401 || errorMessage.includes("permission")) { setError('Private repository detected. Please input PAT.'); - }else if(err.status===404){ + } else if (err.status === 404) { setError('Resource not found.'); - } - else if (errorMessage.includes("validation failed")) { + } else if (errorMessage.includes("validation failed")) { setError('Invalid GitHub username or insufficient permissions.'); - } - else { + } else { setError( 'Unable to fetch GitHub data. Please verify the username, token, or network connection.' ); @@ -70,7 +119,7 @@ export const useGitHubData = (getOctokit: () => any) => { setLoading(false); } }, - [getOctokit] + [getOctokit, cancelPendingRequest] ); return { diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index ce4116fc..50e8e42b 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -33,6 +33,7 @@ import { import { useTheme } from "@mui/material/styles"; import { useGitHubAuth } from "../../hooks/useGitHubAuth"; import { useGitHubData } from "../../hooks/useGitHubData"; +import { useDebounce } from "../../hooks/useDebounce"; const ROWS_PER_PAGE = 10; @@ -79,18 +80,22 @@ const Home: React.FC = () => { const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); - // Fetch data when username, tab, or page changes + // Debounce search filters (300ms delay) to prevent excessive re-renders + const debouncedSearchTitle = useDebounce(searchTitle, 300); + const debouncedSelectedRepo = useDebounce(selectedRepo, 300); + const debouncedStartDate = useDebounce(startDate, 300); + const debouncedEndDate = useDebounce(endDate, 300); + + // Debounce username input (400ms delay) to reduce API calls + const debouncedUsername = useDebounce(username, 400); + + // Auto-fetch data when debounced username, tab, or page changes useEffect(() => { - if (username) { - fetchData(username, page + 1, ROWS_PER_PAGE); + if (debouncedUsername) { + setPage(0); + fetchData(debouncedUsername, 1, ROWS_PER_PAGE); } - }, [tab, page]); - - const handleSubmit = (e: React.FormEvent): void => { - e.preventDefault(); - setPage(0); - fetchData(username, 1, ROWS_PER_PAGE); - }; + }, [tab, debouncedUsername, fetchData]); const handlePageChange = (_: unknown, newPage: number) => { setPage(newPage); @@ -115,24 +120,25 @@ const Home: React.FC = () => { } }); } - if (searchTitle) { + // Use debounced values for filtering + if (debouncedSearchTitle) { filtered = filtered.filter((item) => - item.title.toLowerCase().includes(searchTitle.toLowerCase()) + item.title.toLowerCase().includes(debouncedSearchTitle.toLowerCase()) ); } - if (selectedRepo) { + if (debouncedSelectedRepo) { filtered = filtered.filter((item) => - item.repository_url.includes(selectedRepo) + item.repository_url.includes(debouncedSelectedRepo) ); } - if (startDate) { + if (debouncedStartDate) { filtered = filtered.filter( - (item) => new Date(item.created_at) >= new Date(startDate) + (item) => new Date(item.created_at) >= new Date(debouncedStartDate) ); } - if (endDate) { + if (debouncedEndDate) { filtered = filtered.filter( - (item) => new Date(item.created_at) <= new Date(endDate) + (item) => new Date(item.created_at) <= new Date(debouncedEndDate) ); } return filtered; @@ -165,48 +171,41 @@ const Home: React.FC = () => { return ( - {/* Auth Form */} + {/* Auth Inputs */} -
- - setUsername(e.target.value)} - required - sx={{ flex: 1, minWidth: 150 }} - /> - setToken(e.target.value)} - type="password" - sx={{ flex: 1, minWidth: 150 }} - // Helper link to guide users on generating a GitHub Personal Access Token - helperText={ - - How to generate? - - } - /> - - - -
+ + setUsername(e.target.value)} + placeholder="Start typing to search..." + sx={{ flex: 1, minWidth: 150 }} + /> + setToken(e.target.value)} + type="password" + sx={{ flex: 1, minWidth: 150 }} + helperText={ + + How to generate? + + } + /> +
{/* Filters */} From 1faa61d6a1efe94ef71a60a41c978eb7c07d7d06 Mon Sep 17 00:00:00 2001 From: Tanaya Jadhav Date: Thu, 21 May 2026 10:53:37 +0530 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/pages/Tracker/Tracker.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 23ab1575..8f731609 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -93,11 +93,13 @@ const Home: React.FC = () => { // Auto-fetch data useEffect(() => { - if (debouncedUsername?.trim()) { + const trimmedUsername = debouncedUsername?.trim() ?? ""; + + if (trimmedUsername.length >= 2) { setPage(0); fetchData( - debouncedUsername, + trimmedUsername, 1, ROWS_PER_PAGE, tab === 0 ? "issue" : "pr",