Skip to content

⚡ Bolt: [performance improvement] fix TalksFilterBar debounce#143

Open
anyulled wants to merge 1 commit intomainfrom
bolt/debounce-talks-filter-13049950721628530604
Open

⚡ Bolt: [performance improvement] fix TalksFilterBar debounce#143
anyulled wants to merge 1 commit intomainfrom
bolt/debounce-talks-filter-13049950721628530604

Conversation

@anyulled
Copy link
Copy Markdown
Owner

@anyulled anyulled commented Apr 6, 2026

💡 What:
Fixes a non-functional debounce implementation in the TalksFilterBar component's search input. By using a useRef to store the timeout ID instead of attempting to return a cleanup function directly from the onChange event handler (which React does not execute), we can successfully cancel pending search updates.

🎯 Why:
The previous code resulted in rapid keystrokes triggering a flood of state and URL updates. Since URL updates in Next.js trigger router transitions and layout re-evaluations, this caused massive performance overhead and degraded the typing experience.

📊 Impact:
Significantly reduces React re-renders and Next.js router transitions when a user searches for talks. By actually debouncing the keystrokes (300ms), the UI will remain highly responsive even when typing quickly.

🔬 Measurement:
Start typing continuously into the search input on the talks page. Before this change, the URL query parameters would update with every single letter typed. After this change, the URL will only update 300ms after you stop typing. You can also measure the reduction in React commit phases using the React Profiler.


PR created automatically by Jules for task 13049950721628530604 started by @anyulled

Summary by CodeRabbit

  • Refactor
    • Internal optimization to search filter handling to improve performance.

Fixes a bug where the search input in TalksFilterBar was not being
debounced at all. The `return () => clearTimeout(...)` inside the event
handler is ignored by React. By utilizing a `useRef` to store the timeout
ID, we can properly clear the pending timeout across renders.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Updated TalksFilterBar to manage debounced search timeouts using a persistent useRef instead of local state. Timeout cancellation is now explicitly handled through ref state clearing rather than effect cleanup patterns, changing internal timeout management logic.

Changes

Cohort / File(s) Summary
Debounced Search Refactor
components/layout/TalksFilterBar.tsx
Refactored timeout management from local timeoutId to useRef-based timeoutRef. Each handleSearchChange now clears any pending timeout before scheduling a new one, eliminating previous per-call cleanup functions.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A ref-based rabbit hops with glee,
Timeouts now managed so swiftly and free,
No more cleanup in effect's embrace,
Just persistent refs keeping pace,
Debounces dance in debounced delight,
TalksFilterBar optimized just right! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: fixing a non-functional debounce implementation in TalksFilterBar. It is specific, concise, and accurately reflects the performance improvement objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bolt/debounce-talks-filter-13049950721628530604

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Fix TalksFilterBar search debounce with useRef timeout management

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Fixes non-functional debounce in TalksFilterBar search input
• Uses useRef to properly store and clear timeout IDs
• Eliminates unnecessary URL updates on every keystroke
• Significantly reduces React re-renders and router transitions
Diagram
flowchart LR
  A["User types in search"] -->|onChange event| B["handleSearchChange"]
  B -->|clear previous timeout| C["timeoutRef.current"]
  B -->|set new timeout| D["timeoutRef.current = setTimeout"]
  D -->|after 300ms| E["updateFilters called"]
  E -->|URL updated| F["Router transition"]
Loading

Grey Divider

File Changes

1. components/layout/TalksFilterBar.tsx 🐞 Bug fix +9/-3

Implement proper debounce with useRef timeout management

• Added useRef import to React hooks
• Created timeoutRef state to store timeout ID across renders
• Modified handleSearchChange to clear previous timeout before setting new one
• Removed ineffective cleanup function return statement from event handler

components/layout/TalksFilterBar.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 6, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Stale track in debounce 🐞 Bug ≡ Correctness
Description
The debounced callback in handleSearchChange calls updateFilters(selectedTrack, newQuery) with
selectedTrack captured at the time of typing. If the user clicks a track within the 300ms window,
the pending timeout can later push a URL with the old track, undoing the user’s track selection.
Code

components/layout/TalksFilterBar.tsx[R71-72]

+    timeoutRef.current = setTimeout(() => {
      updateFilters(selectedTrack, newQuery);
Evidence
handleSearchChange schedules a delayed updateFilters call using the current render’s selectedTrack,
while handleTrackClick can immediately change track and push a new URL without cancelling that
pending timeout; when the timeout fires it can overwrite the URL with the previous track value.

components/layout/TalksFilterBar.tsx[56-60]
components/layout/TalksFilterBar.tsx[62-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
A pending debounced search timer can push a URL containing an out-of-date `selectedTrack` if the user changes tracks before the timer fires.

### Issue Context
`handleSearchChange` schedules `updateFilters(selectedTrack, newQuery)` after 300ms, but `handleTrackClick` does not clear any pending timeout. This allows the older scheduled callback to run later and overwrite the newly-selected track in the URL.

### Fix Focus Areas
- components/layout/TalksFilterBar.tsx[56-74]

### Suggested changes
- Clear any pending `timeoutRef.current` at the start of `handleTrackClick`.
- Optionally also keep `selectedTrack` in a ref (updated via `useEffect`) and read from that ref inside the timeout callback, so the callback always uses the latest track.
- (Optional) Clear pending timeout when `searchParams` changes, to avoid applying a debounce based on an obsolete URL snapshot.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Timeout not cleared on unmount 🐞 Bug ☼ Reliability
Description
TalksFilterBar schedules a timeout that can still fire after the component unmounts and call
router.push. This can cause unexpected navigation or side effects after leaving the page.
Code

components/layout/TalksFilterBar.tsx[R71-73]

+    timeoutRef.current = setTimeout(() => {
      updateFilters(selectedTrack, newQuery);
    }, 300);
-    return () => clearTimeout(timeoutId);
Evidence
The component stores the timeout id in a ref and schedules it in handleSearchChange, but the
component has no unmount cleanup to clear timeoutRef.current. A similar filter component
(SpeakersFilterBar) uses a useEffect cleanup to clear its debounce timer.

components/layout/TalksFilterBar.tsx[25-26]
components/layout/TalksFilterBar.tsx[62-74]
components/layout/SpeakersFilterBar.tsx[19-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
A pending debounce timeout can survive component unmount and still execute `updateFilters(...)`, triggering a late `router.push`.

### Issue Context
`timeoutRef.current` is set in `handleSearchChange`, but no effect cleanup clears it when the component unmounts.

### Fix Focus Areas
- components/layout/TalksFilterBar.tsx[25-26]
- components/layout/TalksFilterBar.tsx[62-74]

### Suggested changes
- Add a `useEffect(() => () => { ... }, [])` cleanup that clears `timeoutRef.current` if present and sets it back to `null`.
- (Optional) Also set `timeoutRef.current = null` immediately after clearing in `handleSearchChange` before setting the new timeout, for clarity.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a debouncing mechanism for the search input using useRef. The review feedback suggests refactoring this logic into a useEffect hook to avoid stale closures, ensure proper cleanup on unmount, and more reliably synchronize the search query with the URL.

Comment on lines 62 to 74
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newQuery = e.target.value;
setSearchQuery(newQuery);

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

// Debounce the URL update for search
const timeoutId = setTimeout(() => {
timeoutRef.current = setTimeout(() => {
updateFilters(selectedTrack, newQuery);
}, 300);
return () => clearTimeout(timeoutId);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The current implementation using setTimeout inside the event handler is susceptible to stale closures. For example, if a user types a search query and then quickly selects a track pill before the 300ms timeout completes, the pending search update will execute with the old track value, effectively reverting the user's track selection in the URL. Additionally, this implementation lacks a cleanup mechanism for when the component unmounts.

A more robust approach is to use a useEffect hook to synchronize the searchQuery state with the URL. This ensures the latest state is always used, handles cleanup automatically, and prevents race conditions between search and track filtering.

Suggested change
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newQuery = e.target.value;
setSearchQuery(newQuery);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Debounce the URL update for search
const timeoutId = setTimeout(() => {
timeoutRef.current = setTimeout(() => {
updateFilters(selectedTrack, newQuery);
}, 300);
return () => clearTimeout(timeoutId);
};
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
};
useEffect(() => {
const currentQueryInUrl = searchParams.get("q") || "";
if (searchQuery === currentQueryInUrl) return;
const timer = setTimeout(() => {
updateFilters(selectedTrack, searchQuery);
}, 300);
return () => clearTimeout(timer);
}, [searchQuery, selectedTrack, updateFilters, searchParams]);


import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState, useTransition } from "react";
import { useCallback, useEffect, useState, useTransition, useRef } from "react";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The useRef hook is no longer necessary if the debounce logic is moved to a useEffect hook as suggested below to handle the side effect more robustly.

Suggested change
import { useCallback, useEffect, useState, useTransition, useRef } from "react";
import { useCallback, useEffect, useState, useTransition } from "react";

Comment on lines +71 to 72
timeoutRef.current = setTimeout(() => {
updateFilters(selectedTrack, newQuery);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Stale track in debounce 🐞 Bug ≡ Correctness

The debounced callback in handleSearchChange calls updateFilters(selectedTrack, newQuery) with
selectedTrack captured at the time of typing. If the user clicks a track within the 300ms window,
the pending timeout can later push a URL with the old track, undoing the user’s track selection.
Agent Prompt
### Issue description
A pending debounced search timer can push a URL containing an out-of-date `selectedTrack` if the user changes tracks before the timer fires.

### Issue Context
`handleSearchChange` schedules `updateFilters(selectedTrack, newQuery)` after 300ms, but `handleTrackClick` does not clear any pending timeout. This allows the older scheduled callback to run later and overwrite the newly-selected track in the URL.

### Fix Focus Areas
- components/layout/TalksFilterBar.tsx[56-74]

### Suggested changes
- Clear any pending `timeoutRef.current` at the start of `handleTrackClick`.
- Optionally also keep `selectedTrack` in a ref (updated via `useEffect`) and read from that ref inside the timeout callback, so the callback always uses the latest track.
- (Optional) Clear pending timeout when `searchParams` changes, to avoid applying a debounce based on an obsolete URL snapshot.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/layout/TalksFilterBar.tsx`:
- Around line 25-26: The component's timeoutRef (const timeoutRef =
useRef<ReturnType<typeof setTimeout> | null>(null)) is not cleared on unmount,
allowing the debounced navigation callback to run after teardown; add a
useEffect in TalksFilterBar that returns a cleanup function which clears the
pending timeout (if timeoutRef.current) via clearTimeout and sets
timeoutRef.current = null to prevent the handler from firing after unmount.
Ensure this cleanup runs only on unmount (empty dependency array) and references
timeoutRef exactly as declared.
- Around line 66-73: The pending search debounce can fire with a stale
selectedTrack and overwrite a fresh track selection; modify handleTrackClick to
clear any pending timeoutRef before updating selectedTrack and calling
updateFilters: call clearTimeout(timeoutRef.current) (and set timeoutRef.current
= undefined/null) at the start of handleTrackClick so the existing debounce
callback that captures the old selectedTrack is canceled before you update
selection and call updateFilters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 184b2b58-2240-4afa-a60e-ec62501dcd17

📥 Commits

Reviewing files that changed from the base of the PR and between 81f3c53 and 49aa1a7.

📒 Files selected for processing (1)
  • components/layout/TalksFilterBar.tsx

Comment on lines +25 to +26
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "TalksFilterBar.tsx" -type f

Repository: anyulled/devbcn-nextjs

Length of output: 104


🏁 Script executed:

cat -n components/layout/TalksFilterBar.tsx

Repository: anyulled/devbcn-nextjs

Length of output: 6629


Add unmount cleanup for the debounced timer.

timeoutRef is never cleared when the component unmounts, so the 300ms debounce callback can still execute navigation logic after the component tears down. This should be cleaned up with a useEffect:

Proposed fix
   const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current) {
+        clearTimeout(timeoutRef.current);
+        timeoutRef.current = null;
+      }
+    };
+  }, []);
+
   // Update state when URL changes
   useEffect(() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/TalksFilterBar.tsx` around lines 25 - 26, The component's
timeoutRef (const timeoutRef = useRef<ReturnType<typeof setTimeout> |
null>(null)) is not cleared on unmount, allowing the debounced navigation
callback to run after teardown; add a useEffect in TalksFilterBar that returns a
cleanup function which clears the pending timeout (if timeoutRef.current) via
clearTimeout and sets timeoutRef.current = null to prevent the handler from
firing after unmount. Ensure this cleanup runs only on unmount (empty dependency
array) and references timeoutRef exactly as declared.

Comment on lines +66 to 73
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

// Debounce the URL update for search
const timeoutId = setTimeout(() => {
timeoutRef.current = setTimeout(() => {
updateFilters(selectedTrack, newQuery);
}, 300);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify stale-callback risk patterns in the current file.
rg -n -C3 'const handleTrackClick|setTimeout\(|updateFilters\(selectedTrack,\s*newQuery\)|clearTimeout\(timeoutRef\.current\)' components/layout/TalksFilterBar.tsx

Repository: anyulled/devbcn-nextjs

Length of output: 608


🏁 Script executed:

cat -n components/layout/TalksFilterBar.tsx

Repository: anyulled/devbcn-nextjs

Length of output: 6629


Clear pending search timeout when track selection changes.

When a user types then clicks a track before the 300ms debounce fires, the pending callback still executes with the captured stale selectedTrack value, overwriting the fresh track selection in the URL. Add timeout cleanup to handleTrackClick to prevent this race condition.

Proposed fix
   const handleTrackClick = (track: string) => {
+    if (timeoutRef.current) {
+      clearTimeout(timeoutRef.current);
+      timeoutRef.current = null;
+    }
     const newTrack = track === selectedTrack ? "" : track;
     setSelectedTrack(newTrack);
     updateFilters(newTrack, searchQuery);
   };

   const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const newQuery = e.target.value;
     setSearchQuery(newQuery);

     if (timeoutRef.current) {
       clearTimeout(timeoutRef.current);
+      timeoutRef.current = null;
     }

     // Debounce the URL update for search
     timeoutRef.current = setTimeout(() => {
       updateFilters(selectedTrack, newQuery);
+      timeoutRef.current = null;
     }, 300);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/TalksFilterBar.tsx` around lines 66 - 73, The pending
search debounce can fire with a stale selectedTrack and overwrite a fresh track
selection; modify handleTrackClick to clear any pending timeoutRef before
updating selectedTrack and calling updateFilters: call
clearTimeout(timeoutRef.current) (and set timeoutRef.current = undefined/null)
at the start of handleTrackClick so the existing debounce callback that captures
the old selectedTrack is canceled before you update selection and call
updateFilters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant