ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

React Machine Coding Interview Questions

Short answer

The machine-coding round asks you to build a working component in 30 to 45 minutes while someone watches. The prompts repeat: counter, tabs, modal, debounced search, autocomplete, infinite scroll. What is being scored is state placement, cleanup and keyboard support — not how fast you type.

PromptCore skillThe detail that earns the mark
CounterState basicsFunctional updater for rapid clicks
TabsDerived UIKeyboard arrows and ARIA roles
ModalPortals, effectsEscape key, focus trap, scroll lock
Debounced searchAsync, cleanupAborting the previous request
AutocompleteAsync + keyboardArrow navigation and out-of-order results
Infinite scrollObserversIntersectionObserver, not a scroll listener
Star ratingControlled inputHover preview separate from value
Todo listList stateStable keys and immutable updates
The prompts that repeat, and the one detail each is really testing.

What is the machine coding round actually testing?

Not whether you can build a counter — everyone can. The round exists because writing code while someone watches reveals things an algorithm question cannot: where you put state, whether you clean up after yourself, whether you think about the keyboard, and how you behave when something does not work. Interviewers are usually filling in a rubric with three or four lines on it, and most of them are about judgement rather than output.

The practical consequence is that a half-finished component with clean state modelling and a working keyboard usually scores better than a fully finished one built out of three overlapping `useState` calls and a scroll listener that never gets removed. Narrate as you go — say why state lives where you put it — because a silent candidate gets no credit for reasoning the interviewer cannot see.

Build a counter

The warm-up, and it is genuinely a filter. The trap is reading state directly in the handler, which breaks when updates batch — two clicks in the same tick both read the same stale value and the count only goes up by one.

Counter, with the detail that mattersjsx
1function Counter({ step = 1 }) {
2  const [count, setCount] = useState(0);
3
4  // WRONG: both calls read the same stale `count`
5  const bad = () => { setCount(count + 1); setCount(count + 1); };
6  // → increments by 1
7
8  // RIGHT: the updater form always sees the latest value
9  const inc = () => setCount(c => c + step);
10  const dec = () => setCount(c => Math.max(0, c - step));
11  // → calling inc() twice increments by 2
12
13  return (
14    <div>
15      <button onClick={dec} aria-label="Decrease"></button>
16      <output>{count}</output>
17      <button onClick={inc} aria-label="Increase">+</button>
18    </div>
19  );
20}

Build accessible tabs

Tabs test whether you can derive UI from one piece of state instead of duplicating it. Keep the active tab's id — not its index — because an id survives the list being reordered or filtered. Then render only the active panel. The mark that most candidates miss is keyboard support: a real tab list moves with the arrow keys, and the ARIA roles are what make that expected behaviour.

Tabs with arrow-key navigationjsx
1function Tabs({ items }) {
2  const [active, setActive] = useState(items[0].id);
3
4  const onKeyDown = (e) => {
5    const i = items.findIndex(t => t.id === active);
6    if (e.key === "ArrowRight") setActive(items[(i + 1) % items.length].id);
7    if (e.key === "ArrowLeft")  setActive(items[(i - 1 + items.length) % items.length].id);
8    // → wraps at both ends, which is the expected behaviour
9  };
10
11  return (
12    <>
13      <div role="tablist" onKeyDown={onKeyDown}>
14        {items.map(t => (
15          <button
16            key={t.id}
17            role="tab"
18            aria-selected={t.id === active}
19            tabIndex={t.id === active ? 0 : -1}
20            onClick={() => setActive(t.id)}
21          >
22            {t.label}
23          </button>
24        ))}
25      </div>
26      <div role="tabpanel">{items.find(t => t.id === active).content}</div>
27    </>
28  );
29}

The `tabIndex` line is worth explaining out loud: only the active tab is in the tab order, so pressing Tab moves past the whole tab list rather than through every tab. That is the roving-tabindex pattern, and naming it is a strong signal.

Build a modal

The modal tests effects and cleanup more than layout. Render it through a portal so it escapes any parent with `overflow: hidden` or a stacking context. Close on Escape, close on backdrop click but not on content click, and lock body scroll while open — restoring the previous value rather than blindly setting it back to empty.

Modal with escape, scroll lock and cleanupjsx
1function Modal({ open, onClose, children }) {
2  useEffect(() => {
3    if (!open) return;
4
5    const onKey = (e) => e.key === "Escape" && onClose();
6    document.addEventListener("keydown", onKey);
7
8    const prev = document.body.style.overflow;
9    document.body.style.overflow = "hidden";
10
11    return () => {
12      document.removeEventListener("keydown", onKey);
13      document.body.style.overflow = prev;   // restore, don't assume ""
14    };
15    // → every subscription created here is removed here
16  }, [open, onClose]);
17
18  if (!open) return null;
19
20  return createPortal(
21    <div className="backdrop" onClick={onClose}>
22      <div role="dialog" aria-modal="true" onClick={e => e.stopPropagation()}>
23        {children}
24      </div>
25    </div>,
26    document.body
27  );
28}

Build a debounced search box

This is the prompt that separates candidates most reliably, because there are two bugs and most people only fix one. Debouncing reduces how many requests you fire. It does nothing about responses arriving out of order — a slow request for "re" can land after a fast one for "react" and overwrite the correct results. You need both a debounce and cancellation.

Debounce plus abort, in one effectjsx
1function Search() {
2  const [q, setQ] = useState("");
3  const [results, setResults] = useState([]);
4
5  useEffect(() => {
6    if (!q) { setResults([]); return; }
7
8    const controller = new AbortController();
9    const id = setTimeout(async () => {
10      try {
11        const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`,
12                                { signal: controller.signal });
13        setResults(await res.json());
14      } catch (e) {
15        if (e.name !== "AbortError") throw e;
16      }
17    }, 300);
18
19    // Runs on every keystroke: cancels the pending timer AND any
20    // request already in flight from the previous keystroke.
21    return () => { clearTimeout(id); controller.abort(); };
22    // → exactly one request per pause, and stale ones never land
23  }, [q]);
24
25  return <input value={q} onChange={e => setQ(e.target.value)} />;
26}

Notice there is no separate `debounce` helper. The effect's own cleanup is the debounce — it runs before every re-run and clears the previous timer. That version is shorter than importing Lodash and it avoids the stale-closure bug you get from calling `debounce()` in the component body, where a new debounced function is created on every render.

It is worth saying explicitly why the abort matters, because interviewers often probe it. Without cancellation the bug is intermittent and depends entirely on network timing — it will not appear on your fast local connection and will appear constantly for a user on mobile data. Being able to describe a race condition you cannot reproduce on demand, and then fix it structurally rather than by adding a delay, is the kind of answer that gets remembered.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399

Build an autocomplete

Autocomplete is the debounced search plus keyboard navigation, and it is the most commonly asked prompt at mid-level and above. Track the highlighted index separately from the input value, wrap the arrow keys, commit on Enter and dismiss on Escape.

The keyboard half, which is where the marks arejsx
1const [items, setItems] = useState([]);
2const [highlight, setHighlight] = useState(-1);
3
4const onKeyDown = (e) => {
5  if (e.key === "ArrowDown") {
6    e.preventDefault();                       // stop the caret moving
7    setHighlight(h => (h + 1) % items.length);
8  }
9  if (e.key === "ArrowUp") {
10    e.preventDefault();
11    setHighlight(h => (h - 1 + items.length) % items.length);
12  }
13  if (e.key === "Enter" && highlight >= 0) select(items[highlight]);
14  if (e.key === "Escape") { setItems([]); setHighlight(-1); }
15};
16
17// → highlight resets whenever the query changes, so a stale index
18//   can never point past the end of a shorter result list
19useEffect(() => setHighlight(-1), [items]);

Two extra details separate a good autocomplete from a passable one. Reset the highlighted index whenever the results change, or a stale index can point past the end of a shorter list. And render the dropdown with the right roles — `combobox` on the input, `listbox` on the menu, `aria-activedescendant` pointing at the highlighted option — so a screen reader announces the selection as you arrow through it.

Build infinite scroll

The naive answer is a scroll listener that measures `scrollTop` against `scrollHeight` on every scroll event. It works, and it will cost you marks — it fires hundreds of times a second and forces layout on each one. `IntersectionObserver` is the modern answer: put a sentinel element after the list and load more when it becomes visible.

IntersectionObserver, with a guard against double-loadingjsx
1function Feed() {
2  const [items, setItems] = useState([]);
3  const [page, setPage] = useState(1);
4  const [loading, setLoading] = useState(false);
5  const sentinel = useRef(null);
6
7  useEffect(() => {
8    const el = sentinel.current;
9    if (!el) return;
10
11    const io = new IntersectionObserver(([entry]) => {
12      if (entry.isIntersecting && !loading) setPage(p => p + 1);
13    }, { rootMargin: "200px" });   // start loading slightly early
14
15    io.observe(el);
16    return () => io.disconnect();
17    // → observer torn down on unmount; no listener left behind
18  }, [loading]);
19
20  return (
21    <>
22      {items.map(i => <Row key={i.id} item={i} />)}
23      <div ref={sentinel} />
24    </>
25  );
26}

Build a star rating

A small prompt with one genuinely interesting decision: hover preview and committed value are two different pieces of state. Keep them separate, render whichever applies, and reset the hover on mouse leave.

Two states, not onejsx
1function Rating({ value, onChange, max = 5 }) {
2  const [hover, setHover] = useState(0);
3  const shown = hover || value;   // preview wins while hovering
4
5  return (
6    <div onMouseLeave={() => setHover(0)} role="radiogroup">
7      {Array.from({ length: max }, (_, i) => i + 1).map(n => (
8        <button
9          key={n}
10          role="radio"
11          aria-checked={n === value}
12          onMouseEnter={() => setHover(n)}
13          onClick={() => onChange(n)}
14        >
15          {n <= shown ? "★" : "☆"}
16        </button>
17      ))}
18    </div>
19  );
20}
21// → hovering shows a preview without committing; leaving restores
22//   the real value, because the two were never the same variable

Build a todo list

The oldest prompt there is, and it is still asked because it exposes immutable-update habits in about ninety seconds. Every operation has a copying form: spread to add, filter to remove, map to edit one item. Reaching for push, splice or a direct assignment mutates the existing array, leaves the reference unchanged, and React skips the render — which looks like a broken component rather than a state bug.

The three operations, immutablyjsx
1function Todos() {
2  const [todos, setTodos] = useState([]);
3
4  const add = (text) =>
5    setTodos(t => [...t, { id: crypto.randomUUID(), text, done: false }]);
6
7  const remove = (id) =>
8    setTodos(t => t.filter(x => x.id !== id));
9
10  const toggle = (id) =>
11    setTodos(t => t.map(x => x.id === id ? { ...x, done: !x.done } : x));
12  // → a new array AND a new object for the changed row, so both
13  //   the list and a memoised row re-render correctly
14
15  // WRONG — mutates in place, reference unchanged, nothing renders:
16  // const toggleBad = (id) => {
17  //   const item = todos.find(x => x.id === id);
18  //   item.done = !item.done;
19  //   setTodos(todos);
20  // };
21
22  const remaining = todos.filter(t => !t.done).length;
23  // → derived during render, never stored in state
24
25  return <p>{remaining} left</p>;
26}

The `remaining` line is the detail worth pointing out while you write it. Storing a count in its own state means two values that have to agree, and every future change is a chance for them to drift. Deriving it during render makes that bug impossible, and saying so turns a trivial prompt into evidence that you think about state design.

How should you spend the 45 minutes?

Badly managed time is what sinks otherwise strong candidates. Spend the first two or three minutes clarifying scope out loud — ask whether data is mocked or fetched, whether styling matters, and whether accessibility is in scope. That conversation is itself being assessed, and it prevents you building the wrong thing.

  1. Minutes 0–3: clarify requirements and state the plan aloud.
  2. Minutes 3–8: sketch the component tree and decide where each piece of state lives.
  3. Minutes 8–30: build the happy path end to end — working beats complete.
  4. Minutes 30–38: add the edge cases you named earlier — empty, loading, error.
  5. Minutes 38–45: keyboard and cleanup, then say what you would add with more time.

If you get stuck, say what you are stuck on. Interviewers are allowed to help, and how you use a hint is part of the signal — a candidate who takes a nudge and runs with it reads as someone who will be easy to work with. Silence while you thrash reads as the opposite, and it is the most common way a solvable round goes wrong.

One more thing that costs nothing and reads well: before you declare yourself finished, actually use the component the way a user would. Click it twice quickly, tab through it, empty the input, resize the window. Interviewers notice when a candidate tests their own work unprompted, and it is usually how you catch the one bug that would otherwise be the last thing they remember about you.

Can I use a component library or Lodash?

Ask. Some interviewers allow anything, others want to see the primitive built by hand — and the answer tells you what is being tested. If they say yes, still be able to explain what the library is doing, because "I'd use Radix" without knowing why a focus trap matters answers the wrong question.

Should I use TypeScript in the round?

Use it if the job does and you are fluent. Typed props communicate your data model for free, which is a real advantage. If you are slower in it, use JavaScript — fighting a generic under time pressure costs far more than the types are worth.

How much styling is expected?

Almost none unless they ask. Layout that is not visibly broken is enough. Time spent on CSS is time not spent on the state modelling and keyboard support that the rubric actually lists, so confirm early that visual polish is out of scope.

What if I don't finish?

Very common and usually fine. Finish a coherent slice rather than leaving three half-built features, then say clearly what remains and how you would do it. An interviewer can score a plan they heard; they cannot score code that was never written or explained.

Should I write tests?

Only if asked, or if you have spare time at the end. One meaningful test of the core behaviour is worth more than a suite of shallow ones. Saying which cases you would test is usually enough to get the credit without spending the minutes.

Frequently asked questions

What is a React machine coding interview round?
A live 30–45 minute session where you build a working component while the interviewer watches, usually in a browser sandbox. The prompts are small and familiar — tabs, a modal, a search box — because the point is to observe how you structure state and handle edge cases, not to test knowledge of an obscure API.
What components are most commonly asked?
Counter as a warm-up, then tabs, a modal, a debounced search box, autocomplete, infinite scroll, a star rating and a todo list. Autocomplete is the most common at mid-level and above because it combines async work, keyboard navigation and cleanup in one prompt.
How do I prepare for the machine coding round?
Build each of the common components from scratch, timed, without looking anything up — then rebuild them a week later. The goal is that the mechanics cost you no thought, so your attention during the round goes to state design, edge cases and explaining yourself.
Why should I use the functional updater in setState?
Because React batches updates, so two calls in the same handler both read the same stale value and only one takes effect. setCount(c => c + 1) receives the latest state each time it runs, which makes rapid clicks and successive updates compound correctly.
How do I implement a debounced search in React?
Put a setTimeout inside a useEffect keyed on the query and clear it in the cleanup. The cleanup runs before every re-run, so each keystroke cancels the previous timer — that is the debounce. Create an AbortController in the same effect and abort it in the cleanup so in-flight requests are cancelled too.
Why is IntersectionObserver better than a scroll listener?
A scroll listener fires hundreds of times a second and each measurement of scrollTop or getBoundingClientRect forces a layout calculation. IntersectionObserver is handled by the browser off the main thread and calls you only when visibility actually changes, so it is both simpler and far cheaper.
Why shouldn't I use the array index as a key?
React uses keys to match elements between renders. If items are reordered, filtered or removed from the middle, index keys make React reuse the wrong DOM node — a text input keeps the previous row's value and state attaches to the wrong item. Use a stable id from the data.
How do I make a modal accessible?
Render it in a portal, give it role="dialog" and aria-modal="true", close on Escape, trap Tab within it, and return focus to the element that opened it. Lock body scroll while it is open, restoring the previous overflow value rather than assuming it was empty.
What is the roving tabindex pattern?
Giving the active item tabIndex 0 and every other item -1, so Tab moves past the whole group and the arrow keys move within it. It is the expected keyboard behaviour for tab lists, toolbars and menus, and naming it signals real accessibility experience.
Do I need to handle loading and error states?
Yes, and mention them early even if you build them late. A component that fetches has at least four states — idle, loading, empty and error — and modelling them explicitly rather than with a scatter of booleans is exactly the judgement the round is looking for.
Is it okay to ask the interviewer questions during the round?
Yes, and it is expected. Clarifying scope at the start prevents you building the wrong thing, and asking for a hint when genuinely stuck is better than silence. How you use help is part of what is being assessed.
What if the interviewer asks me to add a feature at the end?
That is usually a deliberate extension to see how your code adapts. If your state was well placed, the change should be small — say so out loud as you make it. If it turns out to be hard, name why, because recognising a design limitation is itself a senior signal.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399
Written by Arun Karthikeyan · Last updated

Full kit

React Interview Kit · ₹399

Get it →