Build infinite scroll
mediumPerformanceHigh freq+220 XP
Render a scrollable list that starts with 20 items, labelled "Item 1" through "Item 20".
Scrolling within 50px of the bottom loads 20 more items, simulating a 300ms network request, up to a hard cap of 60 items total. Past the cap, scrolling to the bottom does nothing.
Examples
// Initial: 20 items, no loading indicator.
// Scroll within 50px of the bottom
// → loading indicator appears
// → 300ms later: 40 items, indicator gone.
// Scroll again → 60 items.
// Scroll again at the cap
// → nothing fetches; no indicator, count stays at 60.Three states: idle, fetching, at-cap. The indicator is only present in the fetching state.
function handleScroll(e) {
const el = e.target;
const nearBottom =
el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
if (!nearBottom || loading || items.length >= MAX_ITEMS) return;
setLoading(true);
setTimeout(() => {
setItems((cur) => [...cur, ...nextPage(cur)]);
setLoading(false);
}, 300);
}The `loading` guard is what prevents a single scroll from firing 10 overlapping fetches.
What it has to do
- The scrollable element has `id="scroll-container"` and a fixed height with `overflow-y: auto` (or `scroll`).
- Each item is an element with `data-testid="list-item"`.
- While a page is loading, exactly one element with `data-testid="loading-indicator"` is present; it is absent otherwise, including once the cap is reached.
- The item count never exceeds 60.
Notes
- `IntersectionObserver` on a sentinel element is cleaner than scroll math — no magic 50px, and no handler running on every scroll frame.
- A fast scroll can fire the handler many times before `setLoading(true)` commits — gate with a ref (`inFlightRef.current`) or an idempotency check, not just state, if you're on an old React.
- Virtualize once the list grows past a few hundred rows (react-virtuoso / react-window). An uncapped infinite list eventually tanks memory and scroll perf.
- Clean up the timeout on unmount; otherwise a user who navigates away mid-fetch triggers a setState-on-unmounted warning when the request finally resolves.
Stuck? The react course covers the ideas this problem is built on.
import { useState } from "react"; const PAGE_SIZE = 20; const MAX_ITEMS = 60; export default function App() { const [items, setItems] = useState( Array.from({ length: PAGE_SIZE }, (_, i) => i + 1) ); const [loading, setLoading] = useState(false); function handleScroll(e) { // TODO: when scrolled within 50px of the bottom, and there is more to // load, simulate a 300ms fetch and append the next 20 items — but never // past MAX_ITEMS, and never while a fetch is already in flight. } return ( <div id="scroll-container" onScroll={handleScroll} style={{ height: 250, overflowY: "auto", border: "1px solid #ddd", fontFamily: "sans-serif" }} > {items.map((n) => ( <div key={n} data-testid="list-item" style={{ padding: 12, borderBottom: "1px solid #eee" }}> Item {n} </div> ))} {loading ? ( <div data-testid="loading-indicator" style={{ padding: 12, textAlign: "center" }}> Loading... </div> ) : null} </div> ); }
- 1.The scrollable element has `id="scroll-container"` and a fixed height with `overflow-y: auto` (or `scroll`).
- 2.Each item is an element with `data-testid="list-item"`.
- 3.While a page is loading, exactly one element with `data-testid="loading-indicator"` is present; it is absent otherwise, including once the cap is reached.
- 4.The item count never exceeds 60.
5 automated assertions run against your rendered component. The exact assertion names are hidden so you build to the behaviour, not to the test.
5 tests · +220 XP
Discussion
Sign in to postSign in to join the conversation.Sign in
No comments yet. Be the first to say something.