Absolutely, this is a classic situation in React, and you're right to be thinking about optimizing data fetching. I'll walk you through both the why and how so that you understand the approach.
The Problem
- You want to “page” through product IDs, fetching their details for each page (e.g., 12 per page).
- When the user changes the sort order or pages forward, you don’t want to refetch data that you have already loaded.
- Simply fetching all products every time is wasteful.
- You wondered about using
useMemo.
useMemo vs. What You Really Need
useMemo is used to memoize expensive calculations between renders, but it does not persist values between renders; it simply avoids recalculating a value if its dependencies have not changed. It's not designed for caching or storing loaded data between renders or requests.
For data caching, especially with async data like Ajax, you want to use component state. You can also use more advanced libraries like TanStack Query (React Query), but let's solve it with state for learning.
How to Approach This
- Cache loaded products in state:
- Maintain a “product cache” in your component, keyed by product ID.
- When loading a new page:
- Check the cache. Fetch only those product IDs whose data you don’t have yet.
- Merge new data into the cache; do not remove previous data.
- Display products for the current page from the cache.
Example Solution
I'll sketch out the main parts, focusing on the data flow:
import React, { useState, useEffect } from "react";
const PAGE_SIZE = 12;
// Mock async fetch for product data
function fetchProductsByIds(ids) {
// Replace with your real AJAX request! This is just an example mock
return new Promise((resolve) => {
setTimeout(() => {
resolve(
ids.map((id) => ({
id,
name: `Product ${id}`,
image: "https://via.placeholder.com/100x100",
link: `/product/${id}`,
// ... other data
}))
);
}, 500);
});
}
function Wheels({ sortedIds, page }) {
const [productCache, setProductCache] = useState({}); // id => product data
// Find out which ids we need for the current page.
const pageStart = (page - 1) * PAGE_SIZE;
const pageEnd = pageStart + PAGE_SIZE;
const pageIds = sortedIds.slice(pageStart, pageEnd);
// Which product data aren't loaded yet?
const missingIds = pageIds.filter((id) => !productCache[id]);
// Fetch missing products when page or sortedIds changes.
useEffect(() => {
if (missingIds.length === 0) return;
fetchProductsByIds(missingIds).then((products) => {
setProductCache((prevCache) => {
// Add new products to existing cache.
const newCache = { ...prevCache };
products.forEach((product) => {
newCache[product.id] = product;
});
return newCache;
});
});
}, [missingIds.join(",")]); // dependency so it runs when ids change
return (
<div>
{pageIds.map((id) => {
const product = productCache[id];
if (!product) return <div key={id}>Loading product {id}...</div>;
return (
<div key={id}>
<img src={product.image} alt={product.name} />
<a href={product.link}>{product.name}</a>
</div>
);
})}
</div>
);
}
App Component (Simplified)
This is an example of how you might wire it up with sort and page state:
function App() {
const [sortOrder, setSortOrder] = useState("default");
const [page, setPage] = useState(1);
// allProductIds would come from your backend or static list
const allProductIds = [...Array(100).keys()].map((i) => i + 1);
// Sort the IDs based on sortOrder
const sortedIds = React.useMemo(() => {
// do your sorting logic here, e.g.,
if (sortOrder === "default") return allProductIds;
if (sortOrder === "high-to-low") return [...allProductIds].reverse();
// ...other sorting
return allProductIds;
}, [sortOrder, allProductIds]);
// Reset page to 1 when sortOrder changes
useEffect(() => setPage(1), [sortOrder]);
return (
<div>
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
>
<option value="default">Default</option>
<option value="high-to-low">Price High to Low</option>
<option value="low-to-high">Price Low to High</option>
{/* etc. */}
</select>
<Wheels sortedIds={sortedIds} page={page} />
{/* Pager control here */}
</div>
);
}
Why This Approach Works
- Efficient: Only fetches data for new, not-yet-cached IDs.
- No re-fetching: Once loaded, product data is cached in the component state, and you only fetch missing products.
- Separation of concerns: The cache is independent of paging, sorting, or filtering, so you don't lose product data already retrieved.
Bonus: React Query, SWR, Etc.
For a production app, consider using TanStack Query, which handles this kind of caching and background fetching for you with almost no code.
But understanding the above approach teaches you the data flow and component state management in React.
Summary
useMemodoes not cache async fetched data, only memoizes calculations between renders.- Use a state object as a cache for fetched products.
- Only fetch what you don't already have, and show loading indicators as needed.
- This pattern is very common in React apps.
Let me know if you want a React Query or SWR version, or have questions about refining this further!