feat: optimize table component with deferred search and dynamic container height

This commit is contained in:
2025-08-14 12:01:38 +02:00
parent 9d64e7c274
commit 40d5f35afb

View File

@@ -1,4 +1,10 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, {
useDeferredValue,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import Cookies from "js-cookie";
import { useQuery } from "@tanstack/react-query";
import { getTableData, readCachedTableData } from "../utils/userHandler";
@@ -13,6 +19,7 @@ interface DataPackage {
adresse: string | null;
plz: string | null;
email: string | null;
_search?: string;
[key: string]: any;
}
@@ -20,14 +27,29 @@ const Table: React.FC = () => {
const [rows, setRows] = useState<DataPackage[]>([]); // holds normalized cache view
const [files, setFiles] = useState<File[]>([]);
const [search, setSearch] = useState("");
const deferredSearch = useDeferredValue(search);
const containerRef = useRef<HTMLDivElement | null>(null);
const firstRowRef = useRef<HTMLTableRowElement | null>(null);
const [rowHeight, setRowHeight] = useState<number>(0);
const [containerHeight, setContainerHeight] = useState<number>();
const [range, setRange] = useState<{ start: number; end: number }>({
start: 0,
end: 0,
});
const OVERSCAN = 100; // Render 100 rows above and below the viewport
const OVERSCAN = 30; // Render 30 rows above and below the viewport
// Build a canonical lowercase search text for a row
const buildSearchText = (r: DataPackage) =>
[
r.losnummer,
r.vorname ?? "",
r.nachname ?? "",
r.adresse ?? "",
r.plz ?? "",
r.email ?? "",
]
.join(" ")
.toLowerCase();
// Einheitliche Input-Styles (nur Tailwind)
const inputClasses =
@@ -41,7 +63,9 @@ const Table: React.FC = () => {
return;
}
// Server könnte entweder ein Objekt oder ein Array liefern
const normalized: DataPackage[] = Array.isArray(cached) ? cached : [cached];
const normalized: DataPackage[] = (
Array.isArray(cached) ? cached : [cached]
).map((r: DataPackage) => ({ ...r, _search: buildSearchText(r) }));
setRows(normalized);
};
@@ -79,22 +103,10 @@ const Table: React.FC = () => {
// Filter rows by search query (case-insensitive)
const filteredRows = useMemo(() => {
const q = search.trim().toLowerCase();
const q = deferredSearch.trim().toLowerCase();
if (!q) return rows;
return rows.filter((r) => {
const values = [
r.losnummer,
r.vorname ?? "",
r.nachname ?? "",
r.adresse ?? "",
r.plz ?? "",
r.email ?? "",
]
.join(" ")
.toLowerCase();
return values.includes(q);
});
}, [rows, search]);
return rows.filter((r) => (r._search ?? buildSearchText(r)).includes(q));
}, [rows, deferredSearch]);
// Measure row height once the first visible row mounts
useEffect(() => {
@@ -137,6 +149,24 @@ const Table: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rowHeight, filteredRows.length]);
// Make the scroll container fill to the bottom of the viewport
useEffect(() => {
const compute = () => {
const el = containerRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const available = Math.max(0, window.innerHeight - rect.top - 16); // 16px bottom breathing room
if (!containerHeight || Math.abs(available - containerHeight) > 1) {
setContainerHeight(available);
requestAnimationFrame(recomputeRange);
}
};
compute();
window.addEventListener("resize", compute);
return () => window.removeEventListener("resize", compute);
// Re-evaluate when content above changes size noticeably
}, [deferredSearch, rows.length]);
// Recompute range whenever data set changes significantly (search, data load)
useEffect(() => {
// Reset scroll position when filter changes to show top of list
@@ -145,7 +175,7 @@ const Table: React.FC = () => {
// Next frame compute range for new data
requestAnimationFrame(recomputeRange);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search, rows.length]);
}, [deferredSearch, rows.length]);
// Handles input changes for table rows
const handleInputChange = (
@@ -155,7 +185,16 @@ const Table: React.FC = () => {
) => {
setRows((prevRows) =>
prevRows.map((row) =>
row.losnummer === losnummer ? { ...row, [field]: value } : row
row.losnummer === losnummer
? {
...row,
[field]: value,
_search: buildSearchText({
...(row as DataPackage),
[field]: value,
} as DataPackage),
}
: row
)
);
};
@@ -200,6 +239,7 @@ const Table: React.FC = () => {
<div
ref={containerRef}
className="overflow-auto rounded-lg shadow ring-1 ring-black/5"
style={{ height: containerHeight }}
>
<table className="min-w-full divide-y divide-gray-200 text-sm">
<thead className="bg-gray-50 sticky top-0">