From 83a2992c0da60c5aae76ce4825c6857a34900896 Mon Sep 17 00:00:00 2001 From: Francisco Pessano Date: Thu, 4 Sep 2025 13:51:08 -0300 Subject: [PATCH] feat: implement debounced search functionality in SpacesGrid component --- apps/web/src/components/spaces-grid.tsx | 60 +++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/spaces-grid.tsx b/apps/web/src/components/spaces-grid.tsx index b250e54..80adf05 100644 --- a/apps/web/src/components/spaces-grid.tsx +++ b/apps/web/src/components/spaces-grid.tsx @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { MoreHorizontal, Plus, Search } from "lucide-react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Tldraw } from "tldraw"; import { NewSpaceDialog } from "@/components/new-space-dialog"; import { Badge } from "@/components/ui/badge"; @@ -69,6 +70,42 @@ export function SpacesGrid() { const spaces = spacesQuery.data ?? []; + // Use an uncontrolled input and a ref-based debounce to avoid rerendering + // on every keystroke. We only update `debouncedSearchTerm` after the + // debounce delay. + const inputRef = useRef(null); + const timerRef = useRef(null); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + const DEBOUNCE_MS = 200; + + useEffect(() => { + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, []); + + const filteredSpaces = useMemo(() => { + const q = debouncedSearchTerm.trim().toLowerCase(); + if (!q) { + return spaces; + } + + return spaces.filter((s) => { + return s.name.toLowerCase().includes(q); + }); + }, [spaces, debouncedSearchTerm]); + + let badgeText = ""; + if (spacesQuery.isLoading) { + badgeText = "Loading…"; + } else if (debouncedSearchTerm) { + badgeText = `${filteredSpaces.length} of ${spaces.length} spaces`; + } else { + badgeText = `${spaces.length} spaces`; + } + return (
{/* Header */} @@ -86,16 +123,31 @@ export function SpacesGrid() {
- + { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + // Use window.setTimeout so the returned id is a number + timerRef.current = window.setTimeout(() => { + const value = inputRef.current?.value ?? ""; + setDebouncedSearchTerm(value); + timerRef.current = null; + }, DEBOUNCE_MS); + }} + placeholder="Search spaces..." + ref={inputRef} + />
- {spacesQuery.isLoading ? "Loading…" : `${spaces.length} spaces`} + {badgeText}
{/* Spaces Grid */}
- {spaces.map((space) => ( + {filteredSpaces.map((space) => ( {/* Empty State (when no spaces exist) */} - {!spacesQuery.isLoading && spaces.length === 0 && ( + {!spacesQuery.isLoading && filteredSpaces.length === 0 && (