optimization

Virtualization for the card list -> Faster loading when searching
This commit is contained in:
2026-03-25 20:05:14 +01:00
parent 3ce4d206d7
commit 7f5dcac1c3
4 changed files with 81 additions and 16 deletions
+26 -4
View File
@@ -9,7 +9,9 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-dom": "^19.2.4",
"react-virtuoso": "^4.18.3",
"react-window": "^2.2.7"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
@@ -1491,9 +1493,9 @@
}
},
"node_modules/flatted": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
@@ -2253,6 +2255,26 @@
"react": "^19.2.4"
}
},
"node_modules/react-virtuoso": {
"version": "4.18.3",
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.18.3.tgz",
"integrity": "sha512-fLz/peHAx4Eu0DLHurFEEI7Y6n5CqEoxBh04rgJM9yMuOJah2a9zWg/MUOmZLcp7zuWYorXq5+5bf3IRgkNvWg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16 || >=17 || >= 18 || >= 19",
"react-dom": ">=16 || >=17 || >= 18 || >=19"
}
},
"node_modules/react-window": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz",
"integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+3 -1
View File
@@ -11,7 +11,9 @@
},
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-dom": "^19.2.4",
"react-virtuoso": "^4.18.3",
"react-window": "^2.2.7"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
+43 -7
View File
@@ -1,11 +1,13 @@
import React, { useContext, useState, memo } from 'react';
import React, { useContext, useState } from 'react';
import { CardContext } from '../../store/CardContext';
function PrintingRow({ card_id, printing }) {
const { ownedAmounts, updateAmount } = useContext(CardContext);
const [loading, setLoading] = useState(false);
const currentAmount = ownedAmounts[card_id]?.[printing.set_id]?.[printing.rarity_id] ?? printing.amount_owned ?? 0;
// Combined key for uniqueness
const key = `${printing.set_id}-${printing.rarity_id}`;
const currentAmount = ownedAmounts[card_id]?.[key] ?? printing.amount_owned ?? 0;
const updateBackend = async (newAmount) => {
const { set_id, rarity_id } = printing;
@@ -16,22 +18,39 @@ function PrintingRow({ card_id, printing }) {
const response = await fetch('http://localhost:3000/collection/amount', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ card_id, set_id, rarity_id, amount_owned: newAmount })
body: JSON.stringify({
card_id,
set_id,
rarity_id,
amount_owned: newAmount
})
});
if (response.ok) {
updateAmount(card_id, set_id, rarity_id, newAmount);
// Update context using the combined key
updateAmount(card_id, key, newAmount);
}
} catch (err) {
// silently fail
} finally {
setLoading(false);
}
};
const increment = () => updateBackend(currentAmount + 1);
const decrement = () => { if (currentAmount > 0) updateBackend(currentAmount - 1); };
const decrement = () => {
if (currentAmount > 0) updateBackend(currentAmount - 1);
};
return (
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.25rem 0', opacity: loading ? 0.6 : 1 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
padding: '0.25rem 0',
opacity: loading ? 0.6 : 1
}}
>
<span>{printing.set_name} {printing.rarity_name}</span>
<div>
<button onClick={decrement} disabled={loading || currentAmount === 0}></button>
@@ -42,4 +61,21 @@ function PrintingRow({ card_id, printing }) {
);
}
export default memo(PrintingRow);
// ✅ Memoize PrintingRow for performance
export default React.memo(
PrintingRow,
(prevProps, nextProps) => {
// Re-render only if card_id changes or printing reference changes
// or if the current amount changed
const prevKey = `${prevProps.printing.set_id}-${prevProps.printing.rarity_id}`;
const nextKey = `${nextProps.printing.set_id}-${nextProps.printing.rarity_id}`;
const prevAmount = prevProps.printing.amount_owned;
const nextAmount = nextProps.printing.amount_owned;
return (
prevProps.card_id === nextProps.card_id &&
prevKey === nextKey &&
prevAmount === nextAmount
);
}
);
+9 -4
View File
@@ -1,4 +1,5 @@
import React, { useEffect, useState, useContext } from 'react';
import { Virtuoso } from 'react-virtuoso';
import CardRow from '../components/CardRow/CardRow';
import SearchBar from '../components/SearchBar/SearchBar';
import { fetchCards } from '../services/api';
@@ -59,12 +60,16 @@ function HomePage() {
return (
<div style={{ display: 'flex', height: '100vh' }}>
{/* Left panel: card list */}
<div style={{ flex: 2, borderRight: '1px solid #ccc', padding: '1rem', overflowY: 'auto' }}>
<div style={{ flex: 2, borderRight: '1px solid #ccc', padding: '1rem' }}>
<h2>Card List</h2>
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
{filteredCards.map(card => (
<CardRow key={card.id} card={card} />
))}
{/* ✅ Virtualized list */}
<Virtuoso
style={{ height: 'calc(100vh - 100px)' }} // Adjust for header/search bar
data={filteredCards}
itemContent={(index, card) => <CardRow key={card.id} card={card} />}
/>
</div>
{/* Right panel: card image */}