perf(PROJ-78): Suchfeld-Debounce + stabile Callbacks gegen Re-Render-Spam
Jeder Tastendruck im Suchfeld setzte bisher sofort State in SearchPage, was über eine instabile doSearch-Referenz einen vollen Re-Render der Ergebnistabelle auslöste (bei page_size 100: 100 Zeilen inkl. sanitizeSnippet pro Tastendruck). Suchbegriff liegt jetzt lokal in der Filterbar und wird debounced (300ms) nach oben propagiert; doSearch liest Filterwerte aus einem Ref statt sie als Dependencies zu führen und bleibt dadurch referenzstabil; SearchResultsTable ist jetzt memoisiert. Enter-Verhalten bleibt sofort (Debounce wird vor dem Submit geflusht), übrige Filter unverändert direkt-kontrolliert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e59b11743a
commit
16bf38876f
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { type SavedSearch } from "@/lib/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -88,13 +89,44 @@ export function SearchFilterBar(props: SearchFilterBarProps) {
|
||||
onDeleteSavedSearch,
|
||||
} = props;
|
||||
|
||||
// PROJ-78: Der Suchbegriff wird lokal gehalten und nur debounced (300 ms)
|
||||
// nach oben propagiert. Dadurch loest Tippen keinen Re-Render der Seite
|
||||
// (und damit der Ergebnistabelle) pro Tastendruck mehr aus.
|
||||
const [localQuery, setLocalQuery] = useState(query);
|
||||
// Letzter zwischen Parent und Input abgeglichener Wert.
|
||||
const syncedRef = useRef(query);
|
||||
|
||||
// Externe Aenderungen (z.B. gespeicherte Suche anwenden) uebernehmen.
|
||||
useEffect(() => {
|
||||
if (query === syncedRef.current) return;
|
||||
syncedRef.current = query;
|
||||
setLocalQuery(query);
|
||||
}, [query]);
|
||||
|
||||
// Tippen debounced nach oben schreiben.
|
||||
useEffect(() => {
|
||||
if (localQuery === syncedRef.current) return;
|
||||
const t = setTimeout(() => {
|
||||
syncedRef.current = localQuery;
|
||||
setQuery(localQuery);
|
||||
}, 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [localQuery, setQuery]);
|
||||
|
||||
// Enter/„Suchen“ darf nicht auf den Debounce warten — Wert sofort flushen.
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
syncedRef.current = localQuery;
|
||||
setQuery(localQuery);
|
||||
onSubmit(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Volltextsuche..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
value={localQuery}
|
||||
onChange={(e) => setLocalQuery(e.target.value)}
|
||||
className="w-full flex-1 sm:w-auto"
|
||||
aria-label="Suchbegriff"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type SearchHit, type SearchMatchField } from "@/lib/api";
|
||||
import { sanitizeSnippet } from "@/lib/sanitize";
|
||||
@@ -61,7 +62,7 @@ interface SearchResultsTableProps {
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
export function SearchResultsTable({
|
||||
function SearchResultsTableInner({
|
||||
results,
|
||||
selected,
|
||||
setSelected,
|
||||
@@ -185,3 +186,7 @@ export function SearchResultsTable({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// PROJ-78: Tippen im Suchfeld aendert keine dieser Props — der teure
|
||||
// Tabellen-Render (inkl. sanitizeSnippet pro Zeile) wird uebersprungen.
|
||||
export const SearchResultsTable = memo(SearchResultsTableInner);
|
||||
|
||||
Reference in New Issue
Block a user