diff --git a/.jules/bolt.md b/.jules/bolt.md index d81fd70a..f240bbd7 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-03-29 - React Event Handler Cleanup Illusion + +**Learning:** Returning a cleanup function (like `() => clearTimeout(id)`) from a React event handler (e.g., `onChange`) does absolutely nothing. React ignores the return value of event handlers. In `TalksFilterBar`, this caused debouncing to fail completely, creating N timeouts for N keystrokes and leading to excessive state updates. +**Action:** When debouncing or throttling inside event handlers without a custom hook, always store the timeout ID in a `useRef` and explicitly call `clearTimeout(ref.current)` before setting the new timeout. diff --git a/__tests__/snapshots/sections/home10/__snapshots__/Section1.test.tsx.snap b/__tests__/snapshots/sections/home10/__snapshots__/Section1.test.tsx.snap index 22bb35c6..b4a6ee26 100644 --- a/__tests__/snapshots/sections/home10/__snapshots__/Section1.test.tsx.snap +++ b/__tests__/snapshots/sections/home10/__snapshots__/Section1.test.tsx.snap @@ -62,7 +62,7 @@ exports[`Section1 Component matches snapshot 1`] = ` alt="" src="/assets/img/icons/calender1.svg" /> - 15th, 16th, & 17th January “2025” + 15th, 16th, & 17th January “2025” diff --git a/components/layout/TalksFilterBar.tsx b/components/layout/TalksFilterBar.tsx index c05acb1f..d2f2e779 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, useRef, useState, useTransition } from "react"; import { motion } from "framer-motion"; interface TalksFilterBarProps { @@ -11,6 +11,7 @@ interface TalksFilterBarProps { export default function TalksFilterBar({ tracks, year: _year }: TalksFilterBarProps) { const router = useRouter(); + const debounceRef = useRef | null>(null); const formatTrackName = (track: string) => { return track.split("(")[0].trim(); @@ -60,11 +61,16 @@ export default function TalksFilterBar({ tracks, year: _year }: TalksFilterBarPr const handleSearchChange = (e: React.ChangeEvent) => { const newQuery = e.target.value; setSearchQuery(newQuery); + + // Clear the previous timeout if it exists + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + // Debounce the URL update for search - const timeoutId = setTimeout(() => { + debounceRef.current = setTimeout(() => { updateFilters(selectedTrack, newQuery); }, 300); - return () => clearTimeout(timeoutId); }; return (