diff --git a/frontend/common/useDebounce.ts b/frontend/common/useDebounce.ts index f7e380e69311..34f12bc2cba4 100644 --- a/frontend/common/useDebounce.ts +++ b/frontend/common/useDebounce.ts @@ -1,26 +1,31 @@ -import { useState } from 'react' +import { useCallback, useEffect, useRef } from 'react' export default function useDebounce(func: any, delay: number) { - const [timeout, saveTimeout] = useState(null) + const timeoutRef = useRef(null) + const funcRef = useRef(func) - const debouncedFunc = function () { - //eslint-disable-next-line - const args = arguments - if (timeout) { - clearTimeout(timeout) - } + // Keep the latest callback without re-creating the debounced function, + // so the caller doesn't have to memoise `func` themselves. + useEffect(() => { + funcRef.current = func + }, [func]) - const newTimeout = setTimeout(function () { - func(...args) - if (newTimeout === timeout) { - saveTimeout(null) - } - }, delay) + useEffect( + () => () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + }, + [], + ) - saveTimeout(newTimeout) - } - - return debouncedFunc as typeof func + return useCallback( + (...args: any[]) => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + timeoutRef.current = setTimeout(() => { + funcRef.current(...args) + }, delay) + }, + [delay], + ) as typeof func } /* Usage example: const searchItems = useDebounce((search:string) => { diff --git a/frontend/web/components/pages/features/FeaturesPage.tsx b/frontend/web/components/pages/features/FeaturesPage.tsx index 43560e0a97b5..6bcec90d8b30 100644 --- a/frontend/web/components/pages/features/FeaturesPage.tsx +++ b/frontend/web/components/pages/features/FeaturesPage.tsx @@ -70,6 +70,7 @@ const FeaturesPage: FC = ({ handleFilterChange, hasFilters, page, + searchResetKey, } = useFeatureFilters(history) const { @@ -208,6 +209,7 @@ const FeaturesPage: FC = ({ onClearFilters={clearFilters} viewMode={viewMode} onViewModeChange={handleViewModeChange} + searchResetKey={searchResetKey} /> ), [ @@ -220,6 +222,7 @@ const FeaturesPage: FC = ({ clearFilters, viewMode, handleViewModeChange, + searchResetKey, ], ) diff --git a/frontend/web/components/pages/features/components/FeaturesTableFilters.tsx b/frontend/web/components/pages/features/components/FeaturesTableFilters.tsx index 23a27075e602..3650d8e0e54e 100644 --- a/frontend/web/components/pages/features/components/FeaturesTableFilters.tsx +++ b/frontend/web/components/pages/features/components/FeaturesTableFilters.tsx @@ -43,6 +43,7 @@ type FeaturesTableFiltersProps = { viewMode?: ViewMode onViewModeChange?: (value: ViewMode) => void excludeTag?: (tag: { type: string; is_permanent: boolean }) => boolean + searchResetKey?: number } export const FeaturesTableFilters: FC = ({ @@ -55,6 +56,7 @@ export const FeaturesTableFilters: FC = ({ onViewModeChange, orgId, projectId, + searchResetKey, viewMode, }) => { const { @@ -107,6 +109,7 @@ export const FeaturesTableFilters: FC = ({
onFilterChange({ search: v || null })} value={search} /> diff --git a/frontend/web/components/pages/features/hooks/useFeatureFilters.ts b/frontend/web/components/pages/features/hooks/useFeatureFilters.ts index e500d1456273..7f088a9295a1 100644 --- a/frontend/web/components/pages/features/hooks/useFeatureFilters.ts +++ b/frontend/web/components/pages/features/hooks/useFeatureFilters.ts @@ -18,6 +18,7 @@ export function useFeatureFilters(history: History): { filters: FilterState page: number hasFilters: boolean + searchResetKey: number handleFilterChange: (updates: Partial) => void clearFilters: () => void goToPage: (newPage: number) => void @@ -29,6 +30,10 @@ export function useFeatureFilters(history: History): { const [filters, setFilters] = useState(initialFilters) const [page, setPage] = useState(initialFilters.page) + // Bumped whenever `filters.search` is reset externally (e.g. Clear Filters), + // so the search input can remount to the fresh value rather than syncing + // state on every keystroke echo. + const [searchResetKey, setSearchResetKey] = useState(0) const updateURLParams = useCallback(() => { const currentParams = Utils.fromParam() @@ -59,6 +64,7 @@ export function useFeatureFilters(history: History): { const newFilters = getFiltersFromParams({}) setFilters(newFilters) setPage(1) + setSearchResetKey((k) => k + 1) }, [history]) const goToPage = (newPage: number) => { @@ -72,5 +78,6 @@ export function useFeatureFilters(history: History): { handleFilterChange, hasFilters: hasActiveFilters(filters), page, + searchResetKey, } } diff --git a/frontend/web/components/tables/TableSearchFilter.tsx b/frontend/web/components/tables/TableSearchFilter.tsx index 21411c72ed35..e58833b3f663 100644 --- a/frontend/web/components/tables/TableSearchFilter.tsx +++ b/frontend/web/components/tables/TableSearchFilter.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useEffect, useState } from 'react' +import React, { FC, useState } from 'react' import Input from 'components/base/forms/Input' import Utils from 'common/utils/utils' import useDebounce from 'common/useDebounce' @@ -9,41 +9,26 @@ type TableFilterType = { onChange: (v: string) => void } -const TableSearchFilter: FC = ({ exact, onChange, value }) => { - const [localValue, setLocalValue] = useState(value) - const searchItems = useDebounce( - useCallback((search: string) => { - if (value !== search) { - onChange(search) - } - //Adding onChange as a dependency here would make this prone to infinite recursion issues - //eslint-disable-next-line - }, []), - 100, +const TableSearchFilter: FC = ({ onChange, value }) => { + const [localValue, setLocalValue] = useState( + (value || '').replace(/^"+|"+$/g, ''), ) + const debouncedOnChange = useDebounce((v: string) => onChange(v), 100) - useEffect(() => { - searchItems(localValue) - }, [localValue]) - - useEffect(() => { - setLocalValue(value || '') - }, [value]) return ( - <> - { - const v = Utils.safeParseEventValue(e) - setLocalValue(v) - }} - value={localValue?.replace(/^"+|"+$/g, '')} - type='text' - className='me-3' - size='xSmall' - placeholder='Search' - search - /> - + { + const v = Utils.safeParseEventValue(e) + setLocalValue(v) + debouncedOnChange(v) + }} + value={localValue} + type='text' + className='me-3' + size='xSmall' + placeholder='Search' + search + /> ) }