Implementing infinite scroll in Entity Lists (like in model-driven PowerApps, SharePoint lists, or custom web apps) is a popular technique to improve performance and user experience by loading data in chunks as the user scrolls down — instead of loading all data at once.
Below is a step-by-step guide (around 1000 words) that explains how to implement infinite scroll for entity lists, along with best practices and technical implementations.
What is Infinite Scroll?
Infinite Scroll is a design pattern where new data is fetched and appended to a list dynamically when the user scrolls to the bottom. It replaces traditional pagination and creates a seamless browsing experience.
Common Use-Cases:
- Product listings (e.g., Amazon, Flipkart)
- Social media feeds (e.g., Facebook, Twitter)
- Activity logs or CRM records
- Search result pages
Step-by-Step Implementation for Infinite Scroll in Entity Lists
Let’s assume you are building a custom web application using React with REST API for a back-end that delivers paginated entity data (like from SQL Server, SharePoint, or Dataverse).
Step 1: Design Backend to Support Paging
Your API must return data in pages, along with metadata like nextPageToken
, page
, or hasMore
.
Sample API Response:
{
"data": [
{ "id": 1, "name": "Record 1" },
{ "id": 2, "name": "Record 2" }
],
"hasMore": true,
"page": 1
}
Node.js with Express Example:
app.get("/api/entityList", async (req, res) => {
const page = parseInt(req.query.page) || 1;
const pageSize = 20;
const records = await db.Entity.find()
.skip((page - 1) * pageSize)
.limit(pageSize);
const total = await db.Entity.countDocuments();
res.json({
data: records,
hasMore: page * pageSize < total,
page
});
});
Step 2: Create the Front-End List Component
Use React or another front-end framework to display your list.
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
Use useEffect
to load initial data:
useEffect(() => {
loadMoreData();
}, []);
Step 3: Load More Data Function
Define a function to load more data from the server.
const loadMoreData = async () => {
if (loading || !hasMore) return;
setLoading(true);
const res = await fetch(`/api/entityList?page=${page}`);
const result = await res.json();
setData(prev => [...prev, ...result.data]);
setHasMore(result.hasMore);
setPage(prev => prev + 1);
setLoading(false);
};
Step 4: Detect Scroll Event
Detect when the user reaches the bottom of the list.
Option A: Window Scroll Listener
useEffect(() => {
const handleScroll = () => {
if (
window.innerHeight + document.documentElement.scrollTop
>= document.documentElement.offsetHeight - 50
) {
loadMoreData();
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [page, hasMore]);
Option B: List Container Scroll (for div containers):
jsCopyEdit<div onScroll={handleScroll} style={{ height: '600px', overflowY: 'scroll' }}>
{data.map(item => <div key={item.id}>{item.name}</div>)}
{loading && <p>Loading more...</p>}
</div>
Step 5: Add Loading Indicator
Let the user know data is being fetched:
{loading && <div>Loading...</div>}
{!hasMore && <div>No more records</div>}
Step 6: Optimize with Debounce or Intersection Observer
Use lodash.debounce
or the modern IntersectionObserver
API for better performance.
IntersectionObserver Example:
const observer = useRef();
const lastItemRef = useCallback(node => {
if (loading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
loadMoreData();
}
});
if (node) observer.current.observe(node);
}, [loading, hasMore]);
Use the ref
in the last item:
{data.map((item, index) => {
if (index === data.length - 1) {
return <div ref={lastItemRef} key={item.id}>{item.name}</div>;
} else {
return <div key={item.id}>{item.name}</div>;
}
})}
Step 7: Handling Empty States and Errors
Include fallback messages:
{!loading && data.length === 0 && <p>No records found</p>}
{error && <p>Error loading data</p>}
Use try/catch inside loadMoreData()
to handle API errors gracefully.
Step 8: Optional Enhancements
- Search + Infinite Scroll: Reset data and call API with search parameters on input change.
- Filters + Sorting: Combine filtering with pagination by passing query params.
- Caching: Cache already loaded data in Redux or localStorage to reduce load.
- Skeleton Loaders: Use placeholders instead of “Loading…” text for smoother UI.
Best Practices
- Always show a loader when fetching more data
- Implement throttling to prevent excessive calls
- Make sure scrolling is accessible and keyboard friendly
- Avoid memory leaks — clear event listeners or observers on unmount
- Consider server-side sorting/filtering to maintain performance
- Maintain scroll position if navigating back to the list
Infinite Scroll vs Pagination
Feature | Infinite Scroll | Pagination |
---|---|---|
UX Flow | Seamless | Controlled and predictable |
Performance | Better for short sessions | Better for large datasets |
Bookmarking | Difficult | Easy |
SEO | Weak unless SSR is used | Strong SEO |
Use Case | Feeds, logs, live data | Reports, search results, admin UIs |
Platforms That Support Infinite Scroll in Entity Lists
- PowerApps (Canvas): Using collections + scrollable gallery + timer or scroll position logic
- SharePoint: Requires custom SPFx or PowerApps integration
- React/Angular Apps: Using REST APIs with list components
- Dataverse (Model-driven apps): Infinite scroll is not natively supported, but paging can mimic it