@@ -40,11 +40,11 @@ import {
4040import { RunnerGroupCard } from "components/runners/RunnerGroupCard" ;
4141import { ParamSelector } from "lib/ParamSelector" ;
4242import { RunnersApiResponse , unknownGoesLast } from "lib/runnerUtils" ;
43+ import debounce from "lodash/debounce" ;
4344import { useSession } from "next-auth/react" ;
4445import { useRouter } from "next/router" ;
45- import { useCallback , useEffect , useMemo , useState } from "react" ;
46- import debounce from "lodash/debounce" ;
4746import type { ParsedUrlQuery } from "querystring" ;
47+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
4848import useSWR from "swr" ;
4949
5050// Define sort order constants to prevent typos
@@ -73,38 +73,61 @@ export default function RunnersPage() {
7373 const orgParam = typeof org === "string" ? org : null ;
7474
7575 const { data : _session , status : _status } = useSession ( ) ;
76-
76+
7777 // Utility function to extract search term from URL query
7878 const getSearchFromQuery = ( query : ParsedUrlQuery ) : string => {
7979 return typeof query . search === "string" ? query . search : "" ;
8080 } ;
8181
8282 // Get search term from URL parameters
83- const [ searchTerm , setSearchTerm ] = useState ( getSearchFromQuery ( router . query ) ) ;
83+ const [ searchTerm , setSearchTerm ] = useState (
84+ getSearchFromQuery ( router . query )
85+ ) ;
8486 const [ sortOrder , setSortOrder ] = useState < SortOrder > ( SORT_ALPHABETICAL ) ;
8587 const [ expandedGroup , setExpandedGroup ] = useState < string | null > ( null ) ;
8688
89+ // Use ref to access current router.query without causing debounce recreations
90+ const routerQueryRef = useRef ( router . query ) ;
91+ routerQueryRef . current = router . query ;
92+
8793 // Sync search state with URL changes
8894 useEffect ( ( ) => {
8995 const searchParam = router . query . search ;
9096 const newSearchTerm = getSearchFromQuery ( { search : searchParam } ) ;
91- setSearchTerm ( newSearchTerm ) ;
92- } , [ router . query . search ] ) ;
97+ // Only update if different to prevent feedback loops
98+ if ( newSearchTerm !== searchTerm ) {
99+ setSearchTerm ( newSearchTerm ) ;
100+ }
101+ } , [ router . query . search , searchTerm ] ) ;
93102
94103 // Debounced function to update search in URL
104+ // Debouncing lets us limit the rate at which the
105+ // function is called, so that it doesn't get called on every
106+ // keystroke. Instead, it'll wait for DEBOUNCE_DELAY_MS
107+ // before executing.
108+ const DEBOUNCE_DELAY_MS = 300 ;
95109 const updateSearchInUrl = useCallback (
96110 debounce ( ( newSearchTerm : string ) => {
97- const query = { ...router . query } ;
111+ const query = { ...routerQueryRef . current } ;
98112 if ( newSearchTerm ) {
99113 query . search = newSearchTerm ;
100114 } else {
101115 delete query . search ;
102116 }
103- router . push ( { pathname : router . pathname , query } , undefined , { shallow : true } ) ;
104- } , 300 ) ,
105- [ router ]
117+ router . push ( { pathname : router . pathname , query } , undefined , {
118+ shallow : true ,
119+ } ) ;
120+ } , DEBOUNCE_DELAY_MS ) ,
121+ [ router . pathname ]
106122 ) ;
107123
124+ // Cleanup debounced function on unmount or when it changes
125+ useEffect ( ( ) => {
126+ return ( ) => {
127+ updateSearchInUrl . cancel ( ) ;
128+ } ;
129+ } , [ updateSearchInUrl ] ) ;
130+
108131 // Handle URL editing for organization
109132 const handleOrgSubmit = ( newOrg : string ) => {
110133 if ( ! newOrg ) return ;
@@ -224,8 +247,8 @@ export default function RunnersPage() {
224247 value = { searchTerm }
225248 onChange = { ( e ) => {
226249 const value = e . target . value ;
227- setSearchTerm ( value ) ; // Immediate UI update
228- updateSearchInUrl ( value ) ; // Debounced URL update
250+ setSearchTerm ( value ) ; // Immediate UI update
251+ updateSearchInUrl ( value ) ; // Debounced URL update
229252 } }
230253 sx = { { maxWidth : 600 , mb : 2 } }
231254 />
0 commit comments