Skip to content

fix: resolve thread dropdown flickering on open and navigation - #10

Merged
aiedwardyi merged 2 commits into
mainfrom
fix/thread-dropdown-glitch
Apr 8, 2026
Merged

aiedwardyi merged 2 commits into
mainfrom
fix/thread-dropdown-glitch

Conversation

@aiedwardyi

@aiedwardyi aiedwardyi commented Apr 8, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes visual glitching in the thread history dropdown where it would flash/flicker when opening or selecting a thread.

  • Eliminate duplicate API fetch on dropdown open (debounced search effect was firing alongside the initial fetch)
  • Add mode="wait" to AnimatePresence to prevent enter/exit animation overlap
  • Guard against toggle during page navigation to prevent re-open flicker
  • Replace autoFocus with deferred ref-based focus to avoid layout thrash
  • Reset search and delete confirmation state on each open
  • Simplify exit animation to opacity-only for cleaner close

Test Plan

  • Open thread dropdown - should appear smoothly without flicker
  • Select a different thread - dropdown closes cleanly, thread loads
  • Select the current thread - dropdown closes without navigation
  • Open/close rapidly - no visual glitching
  • Search threads - debounced, no duplicate requests
  • Test on mobile (375px width)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes flicker/glitching in the thread history dropdown by adjusting fetch timing, animation behavior, focus handling, and navigation/toggle guards.

Changes:

  • Fetch threads on open while preventing overlapping enter/exit animations (AnimatePresence mode="wait" + keyed dropdown).
  • Reset search/delete-confirm state on open and defer focusing the search input.
  • Add navigation/toggle guards and avoid navigating when selecting the currently active thread.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/ThreadDropdown.tsx Outdated
Comment on lines 80 to 99
// Fetch on open, reset search
useEffect(() => {
if (isOpen) fetchThreads()
if (isOpen) {
setSearch("")
setConfirmDeleteId(null)
fetchThreads()
// Focus search input after render
requestAnimationFrame(() => searchInputRef.current?.focus())
}
}, [isOpen, fetchThreads])

// Debounced search
// Debounced search (only when user types, not on open)
const isFirstRender = useRef(true)
useEffect(() => {
if (!isOpen) return
if (!isOpen) { isFirstRender.current = true; return }
// Skip the first render (handled by the open effect above)
if (isFirstRender.current) { isFirstRender.current = false; return }
const timer = setTimeout(() => fetchThreads(search || undefined), 300)
return () => clearTimeout(timer)
}, [search, isOpen, fetchThreads])

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setSearch("") inside the open effect can still trigger a second fetch via the debounced search effect when the previous search value was non-empty. On open: you fetch immediately, then setSearch("") causes a re-render where isFirstRender is already false, so the debounce schedules fetchThreads(undefined) again after 300ms.

To avoid reintroducing duplicate requests, consider moving the search/reset logic into handleToggle (before setting isOpen true) so the open render already has search === "", or add a dedicated “skip next search effect” ref that’s set when programmatically resetting search and consumed by the debounced effect.

Copilot uses AI. Check for mistakes.
Comment thread src/components/ThreadDropdown.tsx Outdated
Comment on lines +82 to +88
if (isOpen) {
setSearch("")
setConfirmDeleteId(null)
fetchThreads()
// Focus search input after render
requestAnimationFrame(() => searchInputRef.current?.focus())
}

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestAnimationFrame(() => searchInputRef.current?.focus()) isn’t canceled on close/unmount. If the dropdown is opened and immediately closed (or the component unmounts during navigation), the queued callback may run after the input is removed, which can lead to inconsistent focus behavior.

Consider storing the RAF id and calling cancelAnimationFrame in the effect cleanup (or gate the focus with an isOpen/mounted ref).

Suggested change
if (isOpen) {
setSearch("")
setConfirmDeleteId(null)
fetchThreads()
// Focus search input after render
requestAnimationFrame(() => searchInputRef.current?.focus())
}
if (!isOpen) return
setSearch("")
setConfirmDeleteId(null)
fetchThreads()
// Focus search input after render
const rafId = requestAnimationFrame(() => searchInputRef.current?.focus())
return () => cancelAnimationFrame(rafId)

Copilot uses AI. Check for mistakes.
Comment thread src/components/ThreadDropdown.tsx Outdated
Comment on lines 114 to 120
// Close immediately without animation to prevent flash during navigation
isNavigatingRef.current = true
setIsOpen(false)
router.push(`/chat?thread=${threadId}`)
// Reset navigation flag after transition
setTimeout(() => { isNavigatingRef.current = false }, 500)
}

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The navigation guard uses a fixed setTimeout(..., 500) to clear isNavigatingRef. This is brittle: route transitions can be faster/slower than 500ms, and it can also leave a dangling timer on unmount.

A more robust approach is to clear the flag based on an actual navigation completion signal (e.g., useEffect that resets when currentThreadId changes, or when route/search params change), and clear any pending timeout in cleanup if you keep the timer.

Copilot uses AI. Check for mistakes.
Comment thread src/components/ThreadDropdown.tsx Outdated
setIsOpen(false)
return
}
// Close immediately without animation to prevent flash during navigation

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment says “Close immediately without animation to prevent flash during navigation”, but the dropdown still exits via AnimatePresence (now opacity-only). This can be misleading when debugging future flicker issues.

Consider updating the comment to reflect the actual behavior (e.g., “close before navigation” or “minimize exit animation”).

Suggested change
// Close immediately without animation to prevent flash during navigation
// Close before navigation to minimize visible flicker during the route change

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@aiedwardyi
aiedwardyi merged commit 92c2c86 into main Apr 8, 2026
5 checks passed
@aiedwardyi
aiedwardyi deleted the fix/thread-dropdown-glitch branch April 8, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants