ReactAdvanced

React Machine Coding Round: Build These Six Components

The hands-on round — debounced search with cancellation, a paginated list, an accessible modal, form validation, a stopwatch, and infinite scroll. Full working implementations plus what the interviewer is scoring.

4 sections · ~45 min · 5-question quiz (pass ≥ 70%)

1How the Machine Coding Round Is Scored

You get 30–60 minutes and a prompt like "build an autocomplete search". The working feature is table stakes; the score comes from what you do around it.

What the interviewer is actually checking:

  1. Do you clarify before coding? Two minutes of questions — "Should search hit an API or filter a local list? Do results need keyboard navigation? Is loading state needed?" — is the strongest opening move available.
  2. Component decomposition. One 200-line component scores far below a small tree of focused ones with a custom hook for the logic.
  3. The three states. Loading, empty, and error. Most candidates ship only the happy path; handling all three is the cheapest differentiator in the round.
  4. Cleanup. Cancelled timers, aborted fetches, removed listeners. Its absence is read as inexperience.
  5. Accessibility basics. A real <button> rather than a clickable <div>, labels tied to inputs, aria-label on icon buttons, visible focus.
  6. Narration. Say what you are doing and why. Silence reads as uncertainty even when the code is right.

Time budget that works: ~5 min clarifying and sketching structure → ~30 min core implementation → ~10 min edge cases and polish → ~5 min walking through your own code and naming what you would add with more time.

Say the trade-offs out loud. "I'm keeping this in local state; if it needed to be shared across routes I'd lift it to context or a store." That single sentence often matters more than the feature itself.

2Debounced Search with Cancellation

The most-asked machine coding problem. It tests debouncing, effect cleanup, race conditions, and loading states all at once.

function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);      // reset the timer on each keystroke
  }, [value, delay]);
  return debounced;
}

function SearchBox() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [status, setStatus] = useState("idle");   // idle | loading | error | done
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (!debouncedQuery.trim()) {
      setResults([]);
      setStatus("idle");
      return;
    }
    const controller = new AbortController();
    setStatus("loading");

    fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`,
          { signal: controller.signal })
      .then((r) => {
        if (!r.ok) throw new Error(`Request failed: ${r.status}`);
        return r.json();
      })
      .then((data) => { setResults(data); setStatus("done"); })
      .catch((err) => { if (err.name !== "AbortError") setStatus("error"); });

    return () => controller.abort();     // cancel the in-flight request
  }, [debouncedQuery]);

  return (
    <div>
      <label htmlFor="search">Search</label>
      <input
        id="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search products…"
      />

      {status === "loading" && <p>Loading…</p>}
      {status === "error" && <p role="alert">Something went wrong. Try again.</p>}
      {status === "done" && results.length === 0 && <p>No results for “{debouncedQuery}”.</p>}

      <ul>
        {results.map((r) => <li key={r.id}>{r.name}</li>)}
      </ul>
    </div>
  );
}

Points scored here: debounce extracted into a reusable hook, AbortController for cancellation, a single status value instead of three booleans that can contradict each other, trimmed empty query short-circuit, a labelled input, and a distinct empty state.

Likely follow-ups: "Add keyboard navigation" (track a highlightedIndex, handle ArrowUp/ArrowDown/Enter/Escape). "What's the difference between debounce and throttle?" — debounce waits for a pause in activity; throttle guarantees at most one call per interval. Search wants debounce; scroll and resize handlers want throttle.

3Pagination and Infinite Scroll

Paginated list — a clean, complete implementation:

function PaginatedList({ pageSize = 10 }) {
  const [page, setPage] = useState(1);
  const [data, setData] = useState({ items: [], total: 0 });
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let ignore = false;
    setLoading(true);
    fetch(`/api/items?page=${page}&limit=${pageSize}`)
      .then((r) => r.json())
      .then((d) => { if (!ignore) setData(d); })
      .finally(() => { if (!ignore) setLoading(false); });
    return () => { ignore = true; };
  }, [page, pageSize]);

  const totalPages = Math.max(1, Math.ceil(data.total / pageSize));

  return (
    <>
      {loading ? <Skeleton /> : (
        <ul>{data.items.map((i) => <li key={i.id}>{i.name}</li>)}</ul>
      )}
      <nav aria-label="Pagination">
        <button onClick={() => setPage((p) => p - 1)} disabled={page === 1}>
          Previous
        </button>
        <span>Page {page} of {totalPages}</span>
        <button
          onClick={() => setPage((p) => p + 1)}
          disabled={page >= totalPages}
        >
          Next
        </button>
      </nav>
    </>
  );
}

Note totalPages is derived during render, not stored in state — exactly the design point from the core concepts course.

Infinite scroll with IntersectionObserver (better than a scroll listener — no throttling needed, and it does not run on the main thread on every pixel):

function InfiniteList() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const sentinelRef = useRef(null);

  useEffect(() => {
    fetch(`/api/items?page=${page}`)
      .then((r) => r.json())
      .then((d) => {
        setItems((prev) => [...prev, ...d.items]);   // updater: never stale
        setHasMore(d.items.length > 0);
      });
  }, [page]);

  useEffect(() => {
    const node = sentinelRef.current;
    if (!node || !hasMore) return;
    const observer = new IntersectionObserver(
      ([entry]) => { if (entry.isIntersecting) setPage((p) => p + 1); },
      { rootMargin: "200px" }              // prefetch before it is visible
    );
    observer.observe(node);
    return () => observer.disconnect();    // cleanup
  }, [hasMore]);

  return (
    <>
      <ul>{items.map((i) => <li key={i.id}>{i.name}</li>)}</ul>
      {hasMore && <div ref={sentinelRef} aria-hidden="true" />}
    </>
  );
}

Follow-up to be ready for: "What if the list is 100,000 rows?" → virtualization (render only the visible window; react-window / @tanstack/react-virtual).

4Modal, Form Validation, and Stopwatch

Accessible modal — portal, Escape key, scroll lock, backdrop click:

function Modal({ isOpen, onClose, title, children }) {
  useEffect(() => {
    if (!isOpen) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = prevOverflow;   // restore, don't hardcode
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div className="backdrop" onClick={onClose}>
      <div
        role="dialog"
        aria-modal="true"
        aria-label={title}
        onClick={(e) => e.stopPropagation()}   // don't close on inner clicks
      >
        <h2>{title}</h2>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>,
    document.body
  );
}

Mention focus trapping and restoring focus to the trigger on close even if you do not have time to implement it — knowing it exists is most of the credit.

Form with validation — validate on blur and on submit, not on every keystroke:

function SignupForm({ onSubmit }) {
  const [values, setValues] = useState({ email: "", password: "" });
  const [touched, setTouched] = useState({});

  const errors = {                                   // derived, not state
    email: !values.email ? "Email is required"
         : !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(values.email) ? "Enter a valid email"
         : null,
    password: values.password.length < 8 ? "At least 8 characters" : null,
  };
  const isValid = Object.values(errors).every((e) => e === null);

  const change = (e) =>
    setValues((v) => ({ ...v, [e.target.name]: e.target.value }));

  function handleSubmit(e) {
    e.preventDefault();
    setTouched({ email: true, password: true });
    if (isValid) onSubmit(values);
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" value={values.email} onChange={change}
             onBlur={() => setTouched((t) => ({ ...t, email: true }))}
             aria-invalid={Boolean(touched.email && errors.email)} />
      {touched.email && errors.email && <span role="alert">{errors.email}</span>}

      <button type="submit" disabled={!isValid}>Sign up</button>
    </form>
  );
}

Errors are computed during render from values — no useEffect syncing an errors state. That is the detail a strong reviewer looks for.

Stopwatch — timers, refs, and cleanup:

function Stopwatch() {
  const [ms, setMs] = useState(0);
  const [running, setRunning] = useState(false);
  const intervalRef = useRef(null);

  useEffect(() => {
    if (!running) return;
    intervalRef.current = setInterval(() => setMs((m) => m + 10), 10);
    return () => clearInterval(intervalRef.current);   // stop & on unmount
  }, [running]);

  const seconds = (ms / 1000).toFixed(2);              // derived

  return (
    <>
      <output>{seconds}s</output>
      <button onClick={() => setRunning((r) => !r)}>
        {running ? "Pause" : "Start"}
      </button>
      <button onClick={() => { setRunning(false); setMs(0); }}>Reset</button>
    </>
  );
}

Note the honest caveat to volunteer: setInterval drifts. For an accurate timer, store a start timestamp and compute elapsed time from Date.now() on each tick. Saying that unprompted is a senior-level signal.

Ready to test yourself?

Sign in to take the quiz, track progress, and earn a certificate.

Sign in