Skip to content

⚡ Bolt: Fix debouncing in TalksFilterBar to prevent redundant route transitions#139

Open
anyulled wants to merge 1 commit intomainfrom
bolt-fix-debounce-talks-filter-14204785367317422303
Open

⚡ Bolt: Fix debouncing in TalksFilterBar to prevent redundant route transitions#139
anyulled wants to merge 1 commit intomainfrom
bolt-fix-debounce-talks-filter-14204785367317422303

Conversation

@anyulled
Copy link
Copy Markdown
Owner

@anyulled anyulled commented Apr 2, 2026

💡 What: Fixed a broken debounce implementation in TalksFilterBar.tsx by using a useRef to store and correctly clear the timeout ID. Added a cleanup useEffect to clear timeouts on unmount.
🎯 Why: The previous code attempted to return a cleanup function from the onChange event handler, which React ignores. This resulted in the timeout never being cleared, meaning every single keystroke fired off a redundant route transition and URL update after 300ms.
📊 Impact: Prevents redundant route transitions and URL state updates when users type quickly in the "Search talks..." input. Reduces main thread blocking and unnecessary React renders, leading to a smoother typing experience.
🔬 Measurement: Type rapidly in the talks search input. Previously, this would queue up N route changes (one for every keystroke). Now, only one route change will occur 300ms after you stop typing. Verified via Playwright script verify_cuj.py.


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

Summary by CodeRabbit

  • Refactor
    • Improved internal event handling in the talks filter to enhance stability and performance.

The previous implementation of `handleSearchChange` returned a cleanup function (`return () => clearTimeout(timeoutId)`), which React ignores for standard event handlers. This meant the timeout was never cleared, and every keystroke queued up a separate, redundant route transition after 300ms.

This commit fixes the issue by storing the `timeoutId` in a `useRef`, explicitly clearing the previous timeout on each keystroke, and adding a proper cleanup effect on component unmount to prevent memory leaks.

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.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
devbcn Ready Ready Preview, Comment Apr 2, 2026 9:04am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

The TalksFilterBar component's debounce timeout management was refactored to use useRef for persistent timer tracking. A cleanup useEffect was added to clear pending timeouts on component unmount, replacing the per-event cleanup function approach.

Changes

Cohort / File(s) Summary
Debounce Timeout Refactoring
components/layout/TalksFilterBar.tsx
Replaced local timeoutId variable with persistent timeoutRef using useRef. Added cleanup useEffect for unmount. Updated handleSearchChange to clear previous timeout before scheduling new one.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A timeout debounced clean,
With refs that hold the in-between,
On unmount, all timers cease,
A rabbit's gift of memory peace! ✨

🚥 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 accurately describes the main change—fixing debouncing in TalksFilterBar to prevent redundant route transitions—which directly matches the changeset's core improvement.

✏️ 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-fix-debounce-talks-filter-14204785367317422303

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 debouncing in TalksFilterBar to prevent redundant transitions

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Fixed broken debounce in TalksFilterBar search handler
• Replaced incorrect cleanup function pattern with useRef
• Added proper cleanup effect to prevent memory leaks
• Eliminates redundant route transitions on rapid typing
Diagram
flowchart LR
  A["Event Handler<br/>handleSearchChange"] -->|"stores timeout in"| B["timeoutRef<br/>useRef"]
  B -->|"clears on each<br/>keystroke"| C["Previous Timeout<br/>Cleared"]
  C -->|"prevents"| D["Redundant Route<br/>Transitions"]
  B -->|"cleanup on<br/>unmount"| E["Memory Leak<br/>Prevention"]
Loading

Grey Divider

File Changes

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

Fix debounce implementation with useRef and cleanup

• Added useRef import to store timeout ID across renders
• Created timeoutRef to store and manage debounce timeout
• Added cleanup effect to clear timeout on component unmount
• Modified handleSearchChange to clear previous timeout before setting new one
• Removed incorrect cleanup function return pattern from event handler

components/layout/TalksFilterBar.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 2, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Debounce not canceled on track 🐞 Bug ≡ Correctness
Description
A pending debounced search timeout is not cleared when a track is clicked, so the timer can fire
later and push a URL built with stale selectedTrack/searchParams, potentially undoing the user's
track selection. This can cause incorrect filters and unexpected route transitions when a user types
then changes track within the debounce window.
Code

components/layout/TalksFilterBar.tsx[R60-82]

    updateFilters(newTrack, searchQuery);
  };

+  // ⚡ Bolt: Clear timeout on unmount to prevent memory leaks
+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current) clearTimeout(timeoutRef.current);
+    };
+  }, []);
+
  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newQuery = e.target.value;
    setSearchQuery(newQuery);
+
+    // ⚡ Bolt: Clear previous timeout before setting a new one
+    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);
Evidence
handleSearchChange schedules a delayed updateFilters(selectedTrack, newQuery) using the render-time
selectedTrack and updateFilters closure, and only clears the timer on subsequent search changes or
unmount. handleTrackClick performs an immediate updateFilters but does not clear a pending timeout;
since querystring pushes keep the component mounted, unmount cleanup will not prevent the older
timer from firing and pushing stale params that can remove/alter the track filter.

components/layout/TalksFilterBar.tsx[34-83]
components/layout/TalksList.tsx[18-31]

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

### Issue description
`TalksFilterBar` debounces search updates via `timeoutRef`, but `handleTrackClick` doesn’t cancel a pending search timeout. If the user types and then changes track within 300ms, the old timeout can fire and push a URL built with stale `selectedTrack`/`searchParams`, potentially undoing the track selection.

### Issue Context
- `handleSearchChange` schedules `setTimeout(() => updateFilters(selectedTrack, newQuery), 300)`.
- Only search changes clear the timeout; track clicks and URL param changes do not.
- Querystring updates typically re-render without unmounting, so unmount cleanup won’t help.

### Fix Focus Areas
- components/layout/TalksFilterBar.tsx[57-83]

### Recommended fix
- In `handleTrackClick`, clear any pending `timeoutRef.current` (and set it to `null`) before calling `updateFilters`.
- Optionally also clear any pending timeout when `searchParams` changes (inside the effect that syncs state from the URL) to avoid stale timers firing after back/forward or other URL-driven updates.
- Consider adding a unit test: type into search (don’t advance timers), click a track, then advance timers and assert the track isn’t reverted by a later push.

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



Remediation recommended

2. Redundant timeout-clear comment 📘 Rule violation ⚙ Maintainability
Description
The newly added comment // ⚡ Bolt: Clear previous timeout before setting a new one restates the
immediately following code instead of explaining non-obvious rationale. This violates the guideline
to keep inline comments focused on intent/rationale rather than narrating behavior.
Code

components/layout/TalksFilterBar.tsx[R74-77]

+    // ⚡ Bolt: Clear previous timeout before setting a new one
+    if (timeoutRef.current) {
+      clearTimeout(timeoutRef.current);
+    }
Evidence
PR Compliance ID 95942 disallows newly added inline comments that simply paraphrase the code’s
behavior. The added comment at TalksFilterBar.tsx describes the next lines that call
clearTimeout(timeoutRef.current) rather than adding non-obvious rationale.

Rule 95942: Restrict inline comments to explaining non-obvious rationale, not restating behavior
components/layout/TalksFilterBar.tsx[74-77]

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 new inline comment restates what the code does (clearing the previous timeout) rather than explaining non-obvious rationale.

## Issue Context
Compliance requires inline comments to explain intent/rationale, not narrate behavior.

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

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



Advisory comments

3. Timeout ref not null-checked 🐞 Bug ⚙ Maintainability
Description
The new timeout clearing uses if (timeoutRef.current) checks and never resets the ref to null,
which is less explicit and can leave a stale timeout ID in the ref after clearing/execution. This
makes the debounce lifecycle harder to reason about and less robust to atypical timer ID values.
Code

components/layout/TalksFilterBar.tsx[R63-77]

+  // ⚡ Bolt: Clear timeout on unmount to prevent memory leaks
+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current) clearTimeout(timeoutRef.current);
+    };
+  }, []);
+
  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newQuery = e.target.value;
    setSearchQuery(newQuery);
+
+    // ⚡ Bolt: Clear previous timeout before setting a new one
+    if (timeoutRef.current) {
+      clearTimeout(timeoutRef.current);
+    }
Evidence
Both the unmount cleanup and the debounce reset branch gate clearTimeout behind a truthiness check
and do not set timeoutRef.current = null after clearing, leaving ambiguous state in the ref.

components/layout/TalksFilterBar.tsx[63-78]

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

### Issue description
`timeoutRef.current` is cleared using truthy checks and isn’t reset to `null` after clearing. This is less explicit than checking for `!== null` and can leave a stale ID in the ref.

### Issue Context
`timeoutRef` is declared as `ReturnType<typeof setTimeout> | null`, so `null` is the correct sentinel value.

### Fix Focus Areas
- components/layout/TalksFilterBar.tsx[63-78]

### Recommended fix
- Replace `if (timeoutRef.current)` with `if (timeoutRef.current !== null)`.
- After `clearTimeout(timeoutRef.current)`, set `timeoutRef.current = null` (in both the unmount cleanup and the “clear previous timeout” branch).

ⓘ 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 adds debouncing to the search input in TalksFilterBar using useRef. Feedback suggests refactoring the implementation to use a useEffect hook instead to prevent potential stale closure issues with selectedTrack and updateFilters, which would also allow for the removal of the useRef import and the timeoutRef variable.

Comment on lines +63 to 83
// ⚡ Bolt: Clear timeout on unmount to prevent memory leaks
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);

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

// ⚡ Bolt: Clear previous timeout before setting a new one
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 debouncing implementation using useRef and manual clearTimeout in the event handler is prone to stale closure issues. The setTimeout callback captures the selectedTrack and updateFilters function from the render cycle when the user started typing. If the user changes the track selection while a search is debouncing, the eventual call to updateFilters will use the old track value and old searchParams, effectively reverting the track selection.

A more robust approach is to use a useEffect to handle the debounced URL update. This ensures that the latest state is always used and that pending updates are correctly cancelled if the state or URL changes in the meantime.

  // ⚡ Bolt: Debounce URL updates when searchQuery changes
  useEffect(() => {
    const currentQuery = searchParams.get("q") || "";
    if (searchQuery === currentQuery) return;

    const timer = setTimeout(() => {
      updateFilters(selectedTrack, searchQuery);
    }, 300);

    return () => clearTimeout(timer);
  }, [searchQuery, selectedTrack, updateFilters, searchParams]);

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


import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState, useTransition } from "react";
import { useCallback, useEffect, useRef, useState, useTransition } 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

Since useRef is no longer needed for the debouncing logic, it can be removed from the React imports.

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

Comment on lines +25 to +26
// ⚡ Bolt: Store timeout ID to properly debounce and prevent redundant API/route calls
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
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 timeoutRef and its associated comment are no longer necessary if the debouncing logic is moved to a useEffect hook, which is a more robust way to handle side effects in React.

Comment on lines 60 to 82
updateFilters(newTrack, searchQuery);
};

// ⚡ Bolt: Clear timeout on unmount to prevent memory leaks
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);

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

// ⚡ Bolt: Clear previous timeout before setting a new one
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.

Action required

1. Debounce not canceled on track 🐞 Bug ≡ Correctness

A pending debounced search timeout is not cleared when a track is clicked, so the timer can fire
later and push a URL built with stale selectedTrack/searchParams, potentially undoing the user's
track selection. This can cause incorrect filters and unexpected route transitions when a user types
then changes track within the debounce window.
Agent Prompt
### Issue description
`TalksFilterBar` debounces search updates via `timeoutRef`, but `handleTrackClick` doesn’t cancel a pending search timeout. If the user types and then changes track within 300ms, the old timeout can fire and push a URL built with stale `selectedTrack`/`searchParams`, potentially undoing the track selection.

### Issue Context
- `handleSearchChange` schedules `setTimeout(() => updateFilters(selectedTrack, newQuery), 300)`.
- Only search changes clear the timeout; track clicks and URL param changes do not.
- Querystring updates typically re-render without unmounting, so unmount cleanup won’t help.

### Fix Focus Areas
- components/layout/TalksFilterBar.tsx[57-83]

### Recommended fix
- In `handleTrackClick`, clear any pending `timeoutRef.current` (and set it to `null`) before calling `updateFilters`.
- Optionally also clear any pending timeout when `searchParams` changes (inside the effect that syncs state from the URL) to avoid stale timers firing after back/forward or other URL-driven updates.
- Consider adding a unit test: type into search (don’t advance timers), click a track, then advance timers and assert the track isn’t reverted by a later push.

ⓘ 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/layout/TalksFilterBar.tsx (1)

57-61: ⚠️ Potential issue | 🟠 Major

Clear pending search debounce when track changes to avoid stale URL overwrite.

At Line 60 you push filters immediately on track click, but a previously scheduled timeout from Lines 80-82 can still fire and call updateFilters with stale selectedTrack captured earlier (Line 81). This can revert the selected track after the user clicks a pill.

💡 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);
   };
@@
-    timeoutRef.current = setTimeout(() => {
+    timeoutRef.current = setTimeout(() => {
       updateFilters(selectedTrack, newQuery);
+      timeoutRef.current = null;
     }, 300);

Also applies to: 74-82

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/TalksFilterBar.tsx` around lines 57 - 61, The click handler
(handleTrackClick) immediately calls updateFilters but doesn't cancel a pending
search debounce, so a previously scheduled timeout can later call updateFilters
with stale selectedTrack; fix by storing the search debounce ID in a
ref/variable (e.g., searchDebounceTimeout or searchDebounceRef) and call
clearTimeout(...) at the start of handleTrackClick before
setSelectedTrack/updateFilters, and do the same in the search-change debounce
logic (the handler that schedules the timeout) so any pending timeout is cleared
whenever the track or search is changed.
🧹 Nitpick comments (1)
components/layout/TalksFilterBar.tsx (1)

25-25: Prefer concise “why-only” comments and drop inline branding markers.

The added comments are mostly describing what the code does; the code is already clear enough without these lines.

As per coding guidelines, "Code must be self-documenting. Only explain why non-obvious decisions were made in comments. DO NOT add inline comments explaining what code does."

Also applies to: 63-64, 74-74

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/TalksFilterBar.tsx` at line 25, Remove the inline
explanatory comments (e.g., "// ⚡ Bolt: Store timeout ID to properly debounce
and prevent redundant API/route calls" and the similar comments at the other
noted locations) and either delete them or replace them with concise "why-only"
comments that explain non-obvious rationale (no implementation-level
descriptions) and strip any branding markers like "⚡ Bolt". Target the comments
around the TalksFilterBar component where timeout/debounce behavior is
implemented so the code remains self-documenting while retaining any brief
rationale for unusual decisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@components/layout/TalksFilterBar.tsx`:
- Around line 57-61: The click handler (handleTrackClick) immediately calls
updateFilters but doesn't cancel a pending search debounce, so a previously
scheduled timeout can later call updateFilters with stale selectedTrack; fix by
storing the search debounce ID in a ref/variable (e.g., searchDebounceTimeout or
searchDebounceRef) and call clearTimeout(...) at the start of handleTrackClick
before setSelectedTrack/updateFilters, and do the same in the search-change
debounce logic (the handler that schedules the timeout) so any pending timeout
is cleared whenever the track or search is changed.

---

Nitpick comments:
In `@components/layout/TalksFilterBar.tsx`:
- Line 25: Remove the inline explanatory comments (e.g., "// ⚡ Bolt: Store
timeout ID to properly debounce and prevent redundant API/route calls" and the
similar comments at the other noted locations) and either delete them or replace
them with concise "why-only" comments that explain non-obvious rationale (no
implementation-level descriptions) and strip any branding markers like "⚡ Bolt".
Target the comments around the TalksFilterBar component where timeout/debounce
behavior is implemented so the code remains self-documenting while retaining any
brief rationale for unusual decisions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5ca3bebf-187a-4e6e-bb7f-70383a02e6e8

📥 Commits

Reviewing files that changed from the base of the PR and between 5009ecd and 71ea94a.

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

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