npm run lint rief next lint auf, das es in Next.js 16 nicht mehr gibt — seit dem Next-16-Upgrade lief effektiv gar kein Lint mehr. Umgestellt auf eslint . mit Flat-Config (eslint.config.mjs statt .eslintrc.json). Der dadurch wieder sichtbare Lint-Lauf zeigte 30 Findings (25 Fehler, 5 Warnungen), alle gefixt: - 19x react-hooks/set-state-in-effect: Loading-States wo möglich als echte Ableitung statt eigenem Effect-State (use-mobile.tsx komplett auf useSyncExternalStore umgebaut), sonst async-Wrapper mit Cancel-Guard um bestehende Loader — Timing/Ladeanzeige unverändert. - react-hooks/refs (useSearch.ts): Ref-Schreibzugriff aus dem Render in einen Effect verschoben. - 4x no-html-link-for-pages: <a href> durch next/link ersetzt in admin/login, forgot-password, signup. - Rest (exhaustive-deps, no-img-element, unused disable) einzeln gefixt. - 4 bewusst belassene disable-Kommentare mit Begründung (shadcn/ui-Datei, QR-Code-data-URL, Full-Reload nach Auth laut Projektregel). eslint-Major-Upgrade auf 10 selbst bleibt blockiert: eslint-plugin-react/ jsx-a11y/import unterstützen ESLint 10 in ihrer aktuellen Latest-Version noch nicht (Crash beim Laden), siehe Feature-Spec PROJ-79. Verifiziert auf 132 (Build-Sandbox, kein Live-Deploy): npm ci/tsc/lint/ build grün, 8 Kern-Routen per Standalone-Server auf HTTP 200 geprüft. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019j28kGcaJAhBnrYX34hGdt
4.5 KiB
id, title, status, created
| id | title | status | created |
|---|---|---|---|
| PROJ-78 | SearchResultsTable re-rendert bei jedem Tastenanschlag im Suchfeld | Deployed | 2026-08-05 |
Problem
SearchResultsTable re-rendert bei jedem Tastenanschlag im Suchfeld, da
query in SearchPage liegt und die Tabelle nicht memoisiert ist. Bei
page_size 100 sind das 100 Zeilen inkl. sanitizeSnippet-Aufruf pro
Tastendruck.
React.memo allein würde hier nichts bringen: onPageChange={(p) => doSearch(p)} ist inline definiert, und doSearch selbst ist ein
useCallback mit allen Filter-States in den Dependencies — die
Memo-Prüfung würde bei jedem Tastendruck ohnehin fehlschlagen, weil sich
die Callback-Referenz ändert.
Ein sauberer Fix bräuchte einen Ref-basierten Umbau von useSearch (z.B.
doSearch per useRef stabil halten oder Debounce am Input statt an
jedem State-Update). Das ist eine Architekturänderung des Such-Hooks und
war im Rahmen der reinen Crash-Härtung-Session ausgeschlossen.
Nur relevant bei großen Ergebnislisten/schwacher Client-Hardware — kein Crash-Risiko.
Lösung (Vorschlag)
useSearch:doSearchstabil halten, z.B. peruseReffür die Filter-State-Werte statt sie alsuseCallback-Dependencies zu führen, oder- Debounce direkt am Such-Input (z.B. 300ms) statt bei jedem Tastendruck sofort neu zu rendern/suchen — reduziert sowohl Re-Renders als auch API-Calls.
SearchResultsTablemitReact.memo+ stabilen Callback-Referenzen umschließen, sobald 1 oder 2 umgesetzt ist.
Implementation Notes
Umgesetzt wurde eine Kombination aus Vorschlag 1–3, bewusst minimal gehalten (kein Context, kein Reducer-Umbau):
1. Debounce am Suchfeld — src/components/search/SearchFilterBar.tsx:96-124
Der Suchbegriff wird jetzt lokal in der Filterbar gehalten (localQuery)
und nur debounced (300 ms) per setQuery nach oben propagiert. Ein
syncedRef merkt sich den zuletzt zwischen Parent und Input abgeglichenen
Wert, damit externe Änderungen (gespeicherte Suche anwenden) weiterhin ins
Feld durchschlagen, ohne eine Rückkopplungsschleife zu erzeugen.
Wirkung: State-Update in SearchPage passiert max. alle 300 ms statt pro
Tastendruck — die gesamte Seite inkl. Tabelle rendert entsprechend seltener.
2. doSearch referenzstabil — src/hooks/useSearch.ts:25-71
Die Filterwerte werden in filtersRef gespiegelt; doSearch hat jetzt
leere Dependencies und liest ausschließlich filtersRef.current. Damit
ändert sich die Callback-Referenz nie mehr.
setQuery ist ein Wrapper (useSearch.ts:47-50), der den Wert synchron
in den Ref schreibt und erst dann den State setzt — dadurch verwendet ein
unmittelbar folgendes doSearch(1) bereits den neuen Suchbegriff.
3. React.memo — src/components/search/SearchResultsTable.tsx:65,190
Komponente in SearchResultsTableInner umbenannt und als
memo(SearchResultsTableInner) exportiert. In
src/app/search/page.tsx:245 wurde das inline onPageChange={(p) => doSearch(p)} durch onPageChange={doSearch} ersetzt.
Alle Props der Tabelle (results, selected, setSelected, total,
page, totalPages, onPageChange) sind beim Tippen jetzt
referenzstabil → der Memo-Vergleich greift, die 100 Zeilen inkl.
sanitizeSnippet werden nicht neu gerendert.
Enter-Verhalten bewusst erhalten: handleSubmit in der Filterbar
flusht den Debounce (setQuery(localQuery)) vor dem Aufruf von
onSubmit — Enter direkt nach dem Tippen sucht weiterhin sofort mit dem
gerade eingegebenen Begriff.
Nicht geändert: Die übrigen Filter (Von/An/Datum/Sortierung/Anhang)
bleiben unverändert direkt-kontrolliert (kein Debounce), Pagination,
gespeicherte Suchen, Export- und Upload-Dialoge, useSavedSearches und
der Initial-Load-Effekt in useSearch.
Verifikation: npm run build (inkl. TypeScript) erfolgreich.
Deployed auf 131 (Produktiv) am 2026-08-05.
Acceptance Criteria
- Tippen im Suchfeld löst kein Re-Render der Ergebnistabelle mehr aus,
solange keine neue Suche ausgelöst wurde.
(Doppelt abgesichert: Debounce verhindert das State-Update in
SearchPage,React.memo+ stabile Props fangen den verbleibenden Fall ab.) - Bestehendes Verhalten (Suche bei Enter/Debounce-Ende, Pagination) funktional unverändert — Enter flusht den Debounce vor der Suche.
- Messbare Verbesserung (z.B. React DevTools Profiler) bei
page_size100. Nicht automatisiert durchführbar — React DevTools Profiler ist im Agent-Kontext nicht verfügbar. Begründung per Code-Analyse siehe Implementation Notes; manuelle Messung offen für QA.