From 89d2220806853dcf3c9015654c5299c6052bc1f4 Mon Sep 17 00:00:00 2001 From: anyulled <100741+anyulled@users.noreply.github.com> Date: Sun, 12 Apr 2026 08:22:12 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Fix=20search=20debounce=20t?= =?UTF-8?q?o=20prevent=20redundant=20URL=20updates=20and=20renders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/bolt.md | 5 +++++ components/layout/TalksFilterBar.tsx | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index d81fd70a..2d6874d2 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -17,3 +17,8 @@ **Learning:** When attempting to optimize an O(N^2) array spread operation (`[...existing, talk]`) inside a grouping loop in `groupTalksByTrack`, the purely functional/immutable constraint specified by the team (and the lack of `Map.groupBy` support in Node 20.x Jest environments) means that we must fall back to immutable reductions. **Action:** When constraints require strict immutability without mutation of objects, use `reduce` with object and array spreads (e.g., `{ ...acc, [key]: [...(acc[key] || []), item] }`) even if it introduces O(N^2) overhead for large arrays. Avoid using `push()` or modifying accumulators directly. Always run Prettier/formatting checks before merge to resolve CI failures. + +## 2026-04-12 - React Event Handler Debouncing Anti-Pattern + +**Learning:** Found a broken debounce implementation in `TalksFilterBar.tsx` where `setTimeout` was called inside an event handler (`handleSearchChange`), and a cleanup function was returned. React event handlers (unlike `useEffect`) do not consume returned cleanup functions. As a result, every keystroke queued a new timer and state update, completely failing to debounce and causing redundant renders/URL updates. +**Action:** Always use a `useRef` to store the timeout ID for debouncing inside event handlers, manually clear it before setting a new one, and clean it up in a `useEffect` on component unmount. diff --git a/components/layout/TalksFilterBar.tsx b/components/layout/TalksFilterBar.tsx index c05acb1f..69351cec 100644 --- a/components/layout/TalksFilterBar.tsx +++ b/components/layout/TalksFilterBar.tsx @@ -1,7 +1,7 @@ "use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useState, useTransition } from "react"; +import { useCallback, useEffect, useState, useTransition, useRef } from "react"; import { motion } from "framer-motion"; interface TalksFilterBarProps { @@ -21,6 +21,15 @@ export default function TalksFilterBar({ tracks, year: _year }: TalksFilterBarPr const [selectedTrack, setSelectedTrack] = useState(searchParams.get("track") || ""); const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || ""); + const searchTimeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + }; + }, []); // Update state when URL changes useEffect(() => { @@ -60,11 +69,15 @@ export default function TalksFilterBar({ tracks, year: _year }: TalksFilterBarPr const handleSearchChange = (e: React.ChangeEvent) => { const newQuery = e.target.value; setSearchQuery(newQuery); + + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + // Debounce the URL update for search - const timeoutId = setTimeout(() => { + searchTimeoutRef.current = setTimeout(() => { updateFilters(selectedTrack, newQuery); }, 300); - return () => clearTimeout(timeoutId); }; return (