AniUI Academy

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.
Asked atMetaLinkedInNetflixUber

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>
  );
}

5 tests · +220 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.