Extract GitHub task search commit debouncing into a hook

Move debounce logic from TaskPage into useGitHubTaskSearchCommit to
prevent excessive GitHub API calls on every keystroke. Uses a 750ms
idle window before committing search values. Add tests for the new hook.
This commit is contained in:
Jinjing
2026-08-07 18:53:01 -07:00
parent 2c865cda66
commit e97152326b
3 changed files with 80 additions and 9 deletions
+12 -9
View File
@@ -431,6 +431,7 @@ import {
type LinearOrderBy,
type LinearViewMode
} from '@/components/task-page-localized-options'
import { useGitHubTaskSearchCommit } from '@/components/use-github-task-search-commit'
function isGitLabMRFilter(value: GitLabTaskFilter | GitLabIssueFilter): value is GitLabTaskFilter {
return value === 'opened' || value === 'merged' || value === 'closed' || value === 'all'
@@ -6533,19 +6534,21 @@ export default function TaskPage(): React.JSX.Element {
]
)
useEffect(() => {
if (!taskResumeApplied) {
return
}
const timeout = window.setTimeout(() => {
const scoped = scopeGitHubTaskSearch(taskSearchInput, activeGithubTaskKind)
const commitTaskSearch = useCallback(
(value: string): void => {
const scoped = scopeGitHubTaskSearch(value, activeGithubTaskKind)
if (scoped !== appliedTaskSearch) {
setTasksFiltering(true)
}
setAppliedTaskSearch(scoped)
}, TASK_SEARCH_DEBOUNCE_MS)
return () => window.clearTimeout(timeout)
}, [activeGithubTaskKind, appliedTaskSearch, taskSearchInput, taskResumeApplied])
},
[activeGithubTaskKind, appliedTaskSearch]
)
useGitHubTaskSearchCommit({
enabled: taskResumeApplied,
onCommit: commitTaskSearch,
value: taskSearchInput
})
useEffect(() => {
if (!taskResumeApplied) {
@@ -0,0 +1,41 @@
// @vitest-environment happy-dom
import { act, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
GITHUB_TASK_SEARCH_IDLE_MS,
useGitHubTaskSearchCommit
} from './use-github-task-search-commit'
describe('useGitHubTaskSearchCommit', () => {
beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.useRealTimers())
it('commits only the final value after a full idle window', () => {
const onCommit = vi.fn()
const view = renderHook(
({ value }) => useGitHubTaskSearchCommit({ enabled: true, onCommit, value }),
{ initialProps: { value: 'r' } }
)
act(() => vi.advanceTimersByTime(400))
view.rerender({ value: 'ra' })
act(() => vi.advanceTimersByTime(400))
view.rerender({ value: 'rate' })
act(() => vi.advanceTimersByTime(GITHUB_TASK_SEARCH_IDLE_MS - 1))
expect(onCommit).not.toHaveBeenCalled()
act(() => vi.advanceTimersByTime(1))
expect(onCommit).toHaveBeenCalledOnce()
expect(onCommit).toHaveBeenCalledWith('rate')
})
it('does not schedule a commit while disabled', () => {
const onCommit = vi.fn()
renderHook(() => useGitHubTaskSearchCommit({ enabled: false, onCommit, value: 'rate' }))
act(() => vi.runAllTimers())
expect(onCommit).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,27 @@
import { useEffect, useRef } from 'react'
// Remote GitHub fan-out needs a longer idle window; Enter still commits immediately.
export const GITHUB_TASK_SEARCH_IDLE_MS = 750
type GitHubTaskSearchCommitOptions = {
enabled: boolean
onCommit: (value: string) => void
value: string
}
export function useGitHubTaskSearchCommit({
enabled,
onCommit,
value
}: GitHubTaskSearchCommitOptions): void {
const onCommitRef = useRef(onCommit)
onCommitRef.current = onCommit
useEffect(() => {
if (!enabled) {
return
}
const timeout = window.setTimeout(() => onCommitRef.current(value), GITHUB_TASK_SEARCH_IDLE_MS)
return () => window.clearTimeout(timeout)
}, [enabled, value])
}