Implement virtual scrolling in Table component for improved performance
- Added useRef to track container and first row for height measurement. - Introduced rowHeight state to dynamically calculate row height. - Implemented range state to manage visible rows based on scroll position. - Added scroll event listener to compute visible range efficiently. - Adjusted rendering logic to display only a subset of rows based on the computed range. - Included top and bottom spacers to maintain table height during scrolling. - Refactored input change handling to remain functional with new rendering logic.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getTableData, readCachedTableData } from "../utils/userHandler";
|
import { getTableData, readCachedTableData } from "../utils/userHandler";
|
||||||
@@ -20,6 +20,14 @@ const Table: React.FC = () => {
|
|||||||
const [rows, setRows] = useState<DataPackage[]>([]); // holds normalized cache view
|
const [rows, setRows] = useState<DataPackage[]>([]); // holds normalized cache view
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const firstRowRef = useRef<HTMLTableRowElement | null>(null);
|
||||||
|
const [rowHeight, setRowHeight] = useState<number>(0);
|
||||||
|
const [range, setRange] = useState<{ start: number; end: number }>({
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
});
|
||||||
|
const OVERSCAN = 100; // Render 100 rows above and below the viewport
|
||||||
|
|
||||||
// Einheitliche Input-Styles (nur Tailwind)
|
// Einheitliche Input-Styles (nur Tailwind)
|
||||||
const inputClasses =
|
const inputClasses =
|
||||||
@@ -69,19 +77,6 @@ const Table: React.FC = () => {
|
|||||||
return () => window.removeEventListener("storage", handler);
|
return () => window.removeEventListener("storage", handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handles input changes for table rows
|
|
||||||
const handleInputChange = (
|
|
||||||
losnummer: string,
|
|
||||||
field: keyof DataPackage,
|
|
||||||
value: string
|
|
||||||
) => {
|
|
||||||
setRows((prevRows) =>
|
|
||||||
prevRows.map((row) =>
|
|
||||||
row.losnummer === losnummer ? { ...row, [field]: value } : row
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter rows by search query (case-insensitive)
|
// Filter rows by search query (case-insensitive)
|
||||||
const filteredRows = useMemo(() => {
|
const filteredRows = useMemo(() => {
|
||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
@@ -101,6 +96,80 @@ const Table: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, [rows, search]);
|
}, [rows, search]);
|
||||||
|
|
||||||
|
// Measure row height once the first visible row mounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (!firstRowRef.current) return;
|
||||||
|
const h = firstRowRef.current.offsetHeight;
|
||||||
|
if (h && h !== rowHeight) setRowHeight(h);
|
||||||
|
}, [firstRowRef.current, rowHeight, range.start]);
|
||||||
|
|
||||||
|
// Compute the current visible range based on scroll position and container size
|
||||||
|
const recomputeRange = () => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const total = filteredRows.length;
|
||||||
|
if (total === 0) {
|
||||||
|
if (range.start !== 0 || range.end !== 0) setRange({ start: 0, end: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rh = rowHeight || 52; // Fallback estimate until measured
|
||||||
|
const scrollTop = container.scrollTop;
|
||||||
|
const containerHeight = container.clientHeight || 0;
|
||||||
|
const firstVisible = Math.floor(scrollTop / rh);
|
||||||
|
const visibleCount = Math.max(1, Math.ceil(containerHeight / rh));
|
||||||
|
const start = Math.max(0, firstVisible - OVERSCAN);
|
||||||
|
const end = Math.min(total, firstVisible + visibleCount + OVERSCAN);
|
||||||
|
if (start !== range.start || end !== range.end) setRange({ start, end });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach scroll listener
|
||||||
|
useEffect(() => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const onScroll = () => {
|
||||||
|
// use requestAnimationFrame to keep it smooth
|
||||||
|
requestAnimationFrame(recomputeRange);
|
||||||
|
};
|
||||||
|
el.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
// Initial compute
|
||||||
|
recomputeRange();
|
||||||
|
return () => el.removeEventListener("scroll", onScroll);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [rowHeight, filteredRows.length]);
|
||||||
|
|
||||||
|
// Recompute range whenever data set changes significantly (search, data load)
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset scroll position when filter changes to show top of list
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (el) el.scrollTop = 0;
|
||||||
|
// Next frame compute range for new data
|
||||||
|
requestAnimationFrame(recomputeRange);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [search, rows.length]);
|
||||||
|
|
||||||
|
// Handles input changes for table rows
|
||||||
|
const handleInputChange = (
|
||||||
|
losnummer: string,
|
||||||
|
field: keyof DataPackage,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
setRows((prevRows) =>
|
||||||
|
prevRows.map((row) =>
|
||||||
|
row.losnummer === losnummer ? { ...row, [field]: value } : row
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// (filteredRows defined above)
|
||||||
|
|
||||||
|
const start = range.start;
|
||||||
|
const end =
|
||||||
|
range.end > 0 ? range.end : Math.min(filteredRows.length, 2 * OVERSCAN);
|
||||||
|
const visibleSlice = filteredRows.slice(start, end);
|
||||||
|
const rh = rowHeight || 52;
|
||||||
|
const topPadding = start * rh;
|
||||||
|
const bottomPadding = Math.max(0, (filteredRows.length - end) * rh);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SubHeaderAdmin
|
<SubHeaderAdmin
|
||||||
@@ -128,7 +197,10 @@ const Table: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
*/}
|
*/}
|
||||||
<div className="overflow-auto rounded-lg shadow ring-1 ring-black/5">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="overflow-auto rounded-lg shadow ring-1 ring-black/5"
|
||||||
|
>
|
||||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
<thead className="bg-gray-50 sticky top-0">
|
<thead className="bg-gray-50 sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -191,10 +263,16 @@ const Table: React.FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{filteredRows.map((row, idx) => (
|
{filteredRows.length > 0 && (
|
||||||
|
<tr key="top-spacer" style={{ height: topPadding }}>
|
||||||
|
<td colSpan={8} />
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{visibleSlice.map((row, i) => (
|
||||||
<tr
|
<tr
|
||||||
key={row.losnummer ?? idx}
|
key={row.losnummer ?? start + i}
|
||||||
className="hover:bg-gray-50 transition-colors"
|
className="hover:bg-gray-50 transition-colors"
|
||||||
|
ref={i === 0 ? firstRowRef : undefined}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-2 font-mono text-xs text-gray-900 w-10 min-w-[2.5rem]">
|
<td className="px-4 py-2 font-mono text-xs text-gray-900 w-10 min-w-[2.5rem]">
|
||||||
<input
|
<input
|
||||||
@@ -289,6 +367,11 @@ const Table: React.FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
{filteredRows.length > 0 && (
|
||||||
|
<tr key="bottom-spacer" style={{ height: bottomPadding }}>
|
||||||
|
<td colSpan={8} />
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user