How to Handle Infinite Scroll in Next.js: Fix and Best Practices
infinite scroll in Next.js, use React hooks like useState and useEffect to load data in pages and detect scroll position. Fetch new data when the user nears the bottom, updating state to append items without reloading the page.Why This Happens
Infinite scroll often breaks when the scroll event is not handled properly or when data fetching logic causes repeated or no loading of new items. A common mistake is not checking if the user reached near the bottom before fetching more data, causing too many requests or no new data loading.
import { useState, useEffect } from 'react'; export default function InfiniteScroll() { const [items, setItems] = useState([]); const [page, setPage] = useState(1); useEffect(() => { fetch(`/api/items?page=${page}`) .then(res => res.json()) .then(newItems => setItems(newItems)); }, [page]); useEffect(() => { const handleScroll = () => { if (window.scrollY + window.innerHeight >= document.body.scrollHeight) { setPage(page + 1); // This uses stale page value } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); return ( <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> ); }
The Fix
Fix the infinite scroll by properly handling the scroll event and updating the page state using a functional update to avoid stale closures. Also, clean up the event listener to prevent memory leaks. This ensures new data loads only when the user scrolls near the bottom.
import { useState, useEffect } from 'react'; export default function InfiniteScroll() { const [items, setItems] = useState([]); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); fetch(`/api/items?page=${page}`) .then(res => res.json()) .then(newItems => { setItems(prev => [...prev, ...newItems]); setLoading(false); }); }, [page]); useEffect(() => { function handleScroll() { if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 && !loading) { setPage(prevPage => prevPage + 1); } } window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [loading]); return ( <> <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} </ul> {loading && <p>Loading more items...</p>} </> ); }
Prevention
To avoid infinite scroll issues, always debounce or throttle scroll events if needed, clean up event listeners, and use functional updates for state changes inside event handlers. Also, track loading state to prevent multiple simultaneous fetches. Testing on different screen sizes helps ensure smooth user experience.
Related Errors
Common related errors include:
- Multiple fetches: Happens when loading state is not tracked, causing repeated API calls.
- Memory leaks: Occur if event listeners are not removed on component unmount.
- Stale closures: Using state variables directly inside event listeners without functional updates causes outdated values.