Skip to the solution
HardFrontend13 min read

Build a Virtualized List with react-window (Fixed Size)

755 views

The problem

Question presented to candidate: "Our contact picker uses react-window's fixed-size list over 100,000 contacts. Rebuild the component it gives us, with the same props, so we understand what it does: 48px rows, a 400px box, five rows of overscan, a scroll-to-row method we can call from outside, and arrow-key navigation through the contacts."

Requirements:

  • A FixedSizeList component with react-window style props: height, itemCount, itemSize, itemData, overscanCount, onItemsRendered, and a row component passed as children that receives index, style and data.
  • An imperative scrollToItem(index, align) exposed through a ref, with "auto", "start", "center" and "end" alignment.
  • Arrow Up/Down, Page Up/Down, Home and End move an active row and keep it in view; a "Jump to #" form scrolls a given row to the top.
  • The active row is exposed to assistive technology, and the scrollbar reflects all 100,000 rows.

What a strong answer should cover:

  • The fixed-size maths (index from scrollOffset / itemSize) and why fixed heights make it O(1).
  • The row contract: the list owns positioning through style, the row owns content, and shared values travel in itemData so a memoised row can skip renders.
  • Stable style objects per index, otherwise React.memo on the row is useless.
  • Alignment rules for scrollToItem, especially that "auto" scrolls only as far as needed, and updating the offset without waiting for the asynchronous scroll event.
  • Keyboard and ARIA: a focusable role="listbox" with aria-activedescendant pointing only at a row that is actually mounted.
  • Awareness that react-window 2 renamed the API (List, rowComponent, rowProps, listRef.scrollToRow).

Clarifying questions expected:

  • "Which react-window major are we on? Version 2 changed the component and prop names."
  • "Does selection need to survive scrolling the active row out of view?"
  • "Is the list height fixed, or should it fill its parent?"

Code / implementation expected: Yes. A reusable list component plus a consumer that wires the ref, keyboard handling and jump form. The real library can be shown as a short usage snippet.

  • React
  • Virtualization
  • react-window
  • Performance

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

13 min read ยท 9 sections

React solution

Every file of the working solution. Open it in the playground to run and change it.

ReactComplete solution
import { memo, useCallback, useMemo, useRef, useState } from "react";
import { FixedSizeList } from "./src/components/FixedSizeList";

const COUNT = 100000;
const FIRST = [
  "Ada",
  "Grace",
  "Linus",
  "Margaret",
  "Alan",
  "Barbara",
  "Ken",
  "Radia",
];
const LAST = [
  "Lovelace",
  "Hopper",
  "Torvalds",
  "Hamilton",
  "Turing",
  "Liskov",
  "Thompson",
  "Perlman",
];

function makeContacts() {
  return Array.from({ length: COUNT }, (_, i) => ({
    id: i + 1,
    name: FIRST[i % 8] + " " + LAST[(i * 3) % 8],
    phone: "+1 555 " + String(1000 + (i % 9000)),
  }));
}

// react-window style row: gets index, style and data, nothing else.
const Row = memo(function Row({ index, style, data }) {
  const contact = data.items[index];
  const active = index === data.active;
  return (
    <div
      style={style}
      id={"contact-" + index}
      role="option"
      aria-selected={active}
      data-testid="row"
      className={"rw-row" + (active ? " rw-active" : "")}
      onClick={() => data.select(index)}
    >
      <span className="rw-num">#{contact.id}</span>
      <span className="rw-name">{contact.name}</span>
      <span className="rw-phone">{contact.phone}</span>
    </div>
  );
});

export default function App() {
  const items = useMemo(makeContacts, []);
  const listRef = useRef(null);
  const [active, setActive] = useState(0);
  const [range, setRange] = useState(null);
  const [jump, setJump] = useState("");

  const select = useCallback((index) => setActive(index), []);
  const itemData = useMemo(
    () => ({ items, active, select }),
    [items, active, select],
  );

  // Only point at the active row while it is actually in the DOM.
  const activeMounted =
    range !== null &&
    active >= range.overscanStartIndex &&
    active <= range.overscanStopIndex;

  function moveTo(index, align = "auto") {
    const i = Math.min(Math.max(0, index), COUNT - 1);
    setActive(i);
    listRef.current.scrollToItem(i, align);
  }

  function onKeyDown(e) {
    const step = { ArrowDown: 1, ArrowUp: -1, PageDown: 8, PageUp: -8 }[e.key];
    if (step) moveTo(active + step);
    else if (e.key === "Home") moveTo(0);
    else if (e.key === "End") moveTo(COUNT - 1);
    else return;
    e.preventDefault();
  }

  function onJump(e) {
    e.preventDefault();
    const n = Number(jump);
    if (Number.isInteger(n) && n >= 1 && n <= COUNT) moveTo(n - 1, "start");
  }

  return (
    <main className="rw-app">
      <h1>100,000 contacts, fixed 48px rows</h1>
      <form className="rw-jump" onSubmit={onJump}>
        <label htmlFor="jump">Jump to #</label>
        <input
          id="jump"
          data-testid="jump"
          type="number"
          min="1"
          max={COUNT}
          value={jump}
          onChange={(e) => setJump(e.target.value)}
        />
        <button data-testid="go" type="submit">
          Go
        </button>
      </form>
      <FixedSizeList
        ref={listRef}
        height={400}
        itemCount={COUNT}
        itemSize={48}
        itemData={itemData}
        overscanCount={5}
        onItemsRendered={setRange}
        className="rw-list"
        data-testid="viewport"
        role="listbox"
        aria-label="Contacts"
        tabIndex={0}
        aria-activedescendant={activeMounted ? "contact-" + active : undefined}
        onKeyDown={onKeyDown}
      >
        {Row}
      </FixedSizeList>
      <p className="rw-status" data-testid="status">
        {range && (
          <>
            Visible {range.visibleStartIndex}-{range.visibleStopIndex}, rendered{" "}
            {range.overscanStartIndex}-{range.overscanStopIndex} ยท{" "}
          </>
        )}
        active #{items[active].id} {items[active].name}
      </p>
    </main>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 87 of 107 in Machine coding

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

Build a Drag-and-drop file uploader with configurable chunk size

Continue