React - Infinite Scroll
Overview
Estimated time: 15–25 minutes
Implement infinite scrolling with a sentinel element that loads more items when visible.
Try it: Infinite list
View source
function App(){
const [items, setItems] = React.useState(() => Array.from({length:20}, (_,i)=> `Row ${i+1}`));
const [hasMore, setHasMore] = React.useState(true);
const sentinelRef = React.useRef(null);
React.useEffect(() => {
const el = sentinelRef.current; if (!el) return;
const io = new IntersectionObserver(async ([e]) => {
if (e.isIntersecting && hasMore){
await new Promise(r => setTimeout(r, 400));
setItems(arr => {
const next = arr.length + 10;
const added = Array.from({length:10}, (_,i)=> `Row ${arr.length + i + 1}`);
if (next >= 100) setHasMore(false);
return [...arr, ...added];
});
}
}, {rootMargin:'200px'});
io.observe(el); return () => io.disconnect();
}, [hasMore]);
return (
<div style={{height:320, overflow:'auto', border:'1px solid var(--border)', padding:8}}>
<ul>{items.map(x => <li key={x}>{x}</li>)}</ul>
<div ref={sentinelRef} aria-hidden="true" style={{height:1}} />
<div aria-live="polite">{hasMore? 'Loading more…' : 'No more items'}</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-infinite')).render(<App />);
Syntax primer
- Place a sentinel element at the end of the list; load when it intersects.
Common pitfalls
- Multiple simultaneous loads—guard with a loading flag.
Exercises
- Replace the mock with a real paginated API and append results.
Checks for Understanding
- What is the purpose of the sentinel element in an infinite list?
- How do you prevent multiple loads from triggering at once?
- When and how should the infinite loader stop requesting more items?
Answers
- It’s observed by IntersectionObserver; when it becomes visible, more items are loaded.
- Use a loading flag or internal guard and update hasMore only when new items are appended; debounce or rootMargin can also help.
- When you reach known total size or an API signals end of data; set hasMore=false to stop observing/loading.