From defe8eac827988d9fb625952c8865678f3683e65 Mon Sep 17 00:00:00 2001 From: hailey Date: Sun, 26 Apr 2026 03:21:17 +0000 Subject: [PATCH] [Search] Debounce autocomplete network query Every keystroke in the right-side desktop search and the composer's mention/emoji autocomplete was firing a fresh searchActorsTypeahead request and re-rendering the full dropdown. Under that per-keystroke render pressure, fast-typing-then-Enter could submit a truncated `query` to the Search screen because the urgent state-update flush wasn't completing between keystrokes. Wrap the incoming `q` in `useDebouncedValue(q, 150)` and use the debounced value for the TanStack queryKey and queryFn. Live `q` still flows through `select` so the search-fallback row and the moderation `isExactMatch` check stay in sync with what the user just typed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Autocomplete/useAutocomplete/index.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/Autocomplete/useAutocomplete/index.ts b/src/components/Autocomplete/useAutocomplete/index.ts index 41faad3db5..f2295c7988 100644 --- a/src/components/Autocomplete/useAutocomplete/index.ts +++ b/src/components/Autocomplete/useAutocomplete/index.ts @@ -2,6 +2,7 @@ import {useCallback} from 'react' import {moderateProfile, type ModerationOpts} from '@atproto/api' import {keepPreviousData, useQuery} from '@tanstack/react-query' +import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {STALE} from '#/state/queries' @@ -15,6 +16,8 @@ import { } from '#/components/Autocomplete/types' import {useEmojiSearch} from './useEmojiSearch' +const QUERY_DEBOUNCE_MS = 150 + const DEFAULT_MOD_OPTS = { userDid: undefined, prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, @@ -34,6 +37,10 @@ export function useAutocomplete({ const agent = useAgent() const moderationOpts = useModerationOpts() const emojiSearch = useEmojiSearch() + // Debounce the value that drives the network request, so we don't refetch + // and re-render the dropdown on every keystroke. Live `q` is still used in + // `select` below for the search-fallback row so it tracks the input. + const debouncedQ = useDebouncedValue(q, QUERY_DEBOUNCE_MS) const query = useQuery({ staleTime: STALE.MINUTES.ONE, @@ -41,19 +48,19 @@ export function useAutocomplete({ 'autocomplete', { type, - query: q, + query: debouncedQ, }, ], async queryFn() { if (type === 'profile') { // TODO return recents - if (!q) return [] + if (!debouncedQ) return [] // Going from "foo" to "foo." should not clear matches. - q = q.toLowerCase().trim().replace(/\.$/, '') + const normalized = debouncedQ.toLowerCase().trim().replace(/\.$/, '') const res = await agent.searchActorsTypeahead({ - q, + q: normalized, limit: limit || 8, }) @@ -64,7 +71,7 @@ export function useAutocomplete({ profile, })) } else if (type === 'emoji') { - return emojiSearch(q, limit || 8) + return emojiSearch(debouncedQ, limit || 8) } return []