react-virtualized to reduce lag

This commit is contained in:
bflatastic committed 2025-04-10 10:50:08 +07:00
1 parent 1ae844259f
commit 7230618ed5
4 files changed
+166 -54

No files matched your search

@@ -4,14 +4,16 @@ import { dndSidePanelWidth, sidePanelWidth } from "~/pages/PageEnigma/signals";
import { H4, P, LoadingDots } from "~/components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEmptySet } from "@fortawesome/pro-solid-svg-icons";
import { AutoSizer, Grid } from "react-virtualized";
import "react-virtualized/styles.css";
interface Props {
busy?: boolean;
className?: string;
debug?: string;
currentPage?: number;
pageSize?: number;
items: MediaItem[];
onLoadMore?: () => void;
hasMore?: boolean;
}
export const ItemElements = ({
@@ -19,40 +21,87 @@ export const ItemElements = ({
className,
debug,
items,
currentPage,
pageSize = 20,
onLoadMore,
hasMore,
}: Props) => {
const displayWidth =
dndSidePanelWidth.value > -1
? dndSidePanelWidth.value
: sidePanelWidth.value;
const displayItems =
currentPage !== undefined
? items.slice(currentPage * pageSize, (currentPage + 1) * pageSize)
: items;
const GRID_GAP = 8;
const ASPECT_RATIO = 4.5 / 5;
const TEXT_HEIGHT = 24;
function getGridColumnsClass(displayWidth: number): string {
const columnCount = getGridColumns(displayWidth);
const rowCount = Math.ceil(items.length / columnCount);
function getGridColumns(displayWidth: number): number {
if (displayWidth <= 280) {
return "grid-cols-2";
return 2;
} else if (displayWidth <= 360) {
return "grid-cols-3";
return 3;
} else if (displayWidth <= 440) {
return "grid-cols-4";
return 4;
} else {
return "grid-cols-4";
return 4;
}
}
const gridColumnsClass = getGridColumnsClass(displayWidth);
const cellRenderer = ({
columnIndex,
rowIndex,
key,
style,
}: {
columnIndex: number;
rowIndex: number;
key: string;
style: React.CSSProperties;
}) => {
const index = rowIndex * columnCount + columnIndex;
if (index >= items.length) return null;
if (busy) {
const adjustedStyle = {
...style,
padding: GRID_GAP / 2,
height: style.height as number,
boxSizing: "border-box" as const,
};
return (
<div key={key} style={adjustedStyle}>
<ItemElement debug={debug} item={items[index]} />
</div>
);
};
const handleScroll = ({
clientHeight,
scrollHeight,
scrollTop,
}: {
clientHeight: number;
scrollHeight: number;
scrollTop: number;
}) => {
if (!onLoadMore || !hasMore || busy) return;
const threshold = 100; // pixels from bottom
const bottom = scrollHeight - clientHeight - scrollTop;
if (bottom < threshold) {
onLoadMore();
}
};
if (busy && items.length === 0) {
return (
<div className="flex h-full w-full">
<LoadingDots className="bg-transparent" />
</div>
);
}
if (items.length === 0 && !busy) {
return (
<div className="flex h-full w-full flex-col items-center justify-center text-center">
@@ -65,13 +114,39 @@ export const ItemElements = ({
</div>
);
}
return (
<div
className={`grid ${gridColumnsClass} gap-2.5 ${className ? " " + className : ""}`}
>
{displayItems.map((item, index) => (
<ItemElement debug={debug} key={index} item={item} />
))}
<div className={`h-full w-full ${className || ""}`}>
<AutoSizer className="w-full">
{({ height, width }) => {
const availableWidth = width - GRID_GAP * (columnCount - 1);
const cellWidth = Math.floor(availableWidth / columnCount);
const imageHeight = Math.floor(cellWidth / ASPECT_RATIO);
const cellHeight = imageHeight + TEXT_HEIGHT;
return (
<Grid
cellRenderer={cellRenderer}
columnCount={columnCount}
columnWidth={cellWidth}
height={height}
rowCount={rowCount}
rowHeight={cellHeight}
width={width}
style={{ outline: "none" }}
columnGap={GRID_GAP}
rowGap={GRID_GAP}
overscanRowCount={2}
onScroll={handleScroll}
/>
);
}}
</AutoSizer>
{busy && hasMore && (
<div className="flex w-full justify-center py-4">
<LoadingDots className="bg-transparent" />
</div>
)}
</div>
);
};
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
import { usePosthogFeatureFlag } from "~/hooks/usePosthogFeatureFlag";
@@ -14,7 +14,6 @@ import {
import {
Button,
FilterButtons,
Pagination,
SearchFilter,
UploadModal3D,
} from "~/components";
@@ -33,6 +32,7 @@ import {
} from "../hooks";
const filterEngineCategories = [FilterEngineCategories.LOCATION];
const PAGE_SIZE = 42; // Load 2 pages worth of items at a time
export const SetsTab = () => {
const showSearchObjectComponent = usePosthogFeatureFlag(
@@ -40,6 +40,7 @@ export const SetsTab = () => {
);
const [openUploadModal, setOpenUploadModal] = useState(false);
const [loadedItemCount, setLoadedItemCount] = useState(PAGE_SIZE);
const { userObjects, userFetchStatus, fetchUserObjects } = useUserObjects({
filterEngineCategories: filterEngineCategories,
@@ -78,15 +79,11 @@ export const SetsTab = () => {
const displayedItems =
selectedFilter === AssetFilterOption.FEATURED
? searchTermForFeaturedObjects
? featuredObjectsSearchResults ?? []
: featuredObjects ?? []
? (featuredObjectsSearchResults ?? [])
: (featuredObjects ?? [])
: searchTermForUserObjects
? userObjectsSearchResults ?? []
: userObjects ?? [];
const [currentPage, setCurrentPage] = useState<number>(0);
const pageSize = 21;
const totalPages = Math.ceil(displayedItems.length / pageSize);
? (userObjectsSearchResults ?? [])
: (userObjects ?? []);
const isFetching = isAnyStatusFetching([
userFetchStatus,
@@ -95,17 +92,17 @@ export const SetsTab = () => {
userObjectsSearchFetchStatus,
]);
useEffect(() => {
if (searchTermForUserObjects.length > 0) {
setCurrentPage(0);
const handleLoadMore = useCallback(() => {
if (!isFetching && loadedItemCount < displayedItems.length) {
setLoadedItemCount((prev) =>
Math.min(prev + PAGE_SIZE, displayedItems.length),
);
}
}, [searchTermForUserObjects]);
}, [isFetching, loadedItemCount, displayedItems.length]);
useEffect(() => {
if (searchTermForFeaturedObjects.length > 0) {
setCurrentPage(0);
}
}, [searchTermForFeaturedObjects]);
setLoadedItemCount(PAGE_SIZE);
}, [searchTermForUserObjects, searchTermForFeaturedObjects, selectedFilter]);
return (
<>
@@ -152,21 +149,11 @@ export const SetsTab = () => {
<ItemElements
busy={isFetching}
debug="sets tab"
currentPage={currentPage}
pageSize={pageSize}
items={displayedItems}
items={displayedItems.slice(0, loadedItemCount)}
onLoadMore={handleLoadMore}
hasMore={loadedItemCount < displayedItems.length}
/>
</div>
{totalPages > 1 && (
<Pagination
className="-mt-4 px-4"
currentPage={currentPage}
totalPages={totalPages}
onPageChange={(newPage: number) => {
setCurrentPage(newPage);
}}
/>
)}
<UploadModal3D
onClose={() => setOpenUploadModal(false)}
onSuccess={fetchUserObjects}
+50 -2
View File
@@ -25,6 +25,7 @@
"@remix-run/serve": "^2.8.1",
"@tweenjs/tween.js": "^18.6.4",
"@types/lodash": "^4.17.6",
"@types/react-virtualized": "^9.22.2",
"@zip.js/zip.js": "^2.7.51",
"ammo.js": "^0.0.10",
"ccapture.js": "github:aceslowman/ccapture.js#npm-fix",
@@ -44,6 +45,7 @@
"react-drag-drop-files": "^2.3.10",
"react-helmet": "^6.1.0",
"react-image-crop": "^11.0.5",
"react-virtualized": "^9.22.6",
"tailwind-merge": "^2.2.2",
"terser": "^5.30.3",
"three": "^0.162.0",
@@ -4589,14 +4591,12 @@
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -4633,6 +4633,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-virtualized": {
"version": "9.22.2",
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.22.2.tgz",
"integrity": "sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==",
"dependencies": {
"@types/prop-types": "*",
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
@@ -7195,6 +7204,14 @@
"node": ">=0.8"
}
},
"node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"engines": {
"node": ">=6"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -7996,6 +8013,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/domexception": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
@@ -18824,6 +18850,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -18874,6 +18905,23 @@
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-virtualized": {
"version": "9.22.6",
"resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz",
"integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
"dependencies": {
"@babel/runtime": "^7.7.2",
"clsx": "^1.0.4",
"dom-helpers": "^5.1.3",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+2
View File
@@ -37,6 +37,7 @@
"@remix-run/serve": "^2.8.1",
"@tweenjs/tween.js": "^18.6.4",
"@types/lodash": "^4.17.6",
"@types/react-virtualized": "^9.22.2",
"@zip.js/zip.js": "^2.7.51",
"ammo.js": "^0.0.10",
"ccapture.js": "github:aceslowman/ccapture.js#npm-fix",
@@ -56,6 +57,7 @@
"react-drag-drop-files": "^2.3.10",
"react-helmet": "^6.1.0",
"react-image-crop": "^11.0.5",
"react-virtualized": "^9.22.6",
"tailwind-merge": "^2.2.2",
"terser": "^5.30.3",
"three": "^0.162.0",