LearnFrenzy
Sign in Take a free test

React interview · Set 7 of 8

React Machine Coding Interview Questions

  • 6 follow-up questions
  • 4 key points
  • Code examples
Test yourself

Build it live, build it right

Machine-coding rounds reward clear state, immutable updates, cleanup of timers and requests, and accessibility. Talk through the state shape before typing.

  • StateTodo, accordion, tabs and rating.
  • EffectsDebounce, timers and infinite scroll.
  • AccessibilityLabels, ARIA and native dialog.

Machine-coding rounds give you 45–90 minutes to build a working component. Interviewers judge state design, clean components, accessibility and edge cases. Each solution below is short enough to write live and complete enough to impress.

1. Build a todo list with add, toggle and delete.

One array of objects in state, immutable updates, stable ids as keys, and an accessible form.

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState('');

  function add(e) {
    e.preventDefault();
    if (!text.trim()) return;
    setTodos(t => [...t, { id: crypto.randomUUID(), title: text.trim(), done: false }]);
    setText('');
  }
  const toggle = id => setTodos(t => t.map(x => (x.id === id ? { ...x, done: !x.done } : x)));
  const remove = id => setTodos(t => t.filter(x => x.id !== id));

  return (
    <form onSubmit={add}>
      <label htmlFor="new-todo">New todo</label>
      <input id="new-todo" value={text} onChange={e => setText(e.target.value)} />
      <button>Add</button>
      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} aria-label={`Mark ${t.title} done`} />
            <span style={{ textDecoration: t.done ? 'line-through' : 'none' }}>{t.title}</span>
            <button type="button" onClick={() => remove(t.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </form>
  );
}

2. Build a debounced search that ignores stale responses.

Debounce the query with an effect timer, then fetch with an AbortController so an older, slower response cannot overwrite a newer one.

function useDebounced(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const q = useDebounced(query);

  useEffect(() => {
    if (!q) { setResults([]); return; }
    const controller = new AbortController();
    fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal })
      .then(r => r.json()).then(setResults)
      .catch(e => { if (e.name !== 'AbortError') console.error(e); });
    return () => controller.abort();
  }, [q]);

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search courses" />
      <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
    </>
  );
}

3. Build an accessible star rating component.

Controlled value from the parent, hover preview, keyboard support through native radio inputs.

function StarRating({ value, onChange, max = 5 }) {
  const [hover, setHover] = useState(0);
  return (
    <div role="radiogroup" aria-label="Rating" onMouseLeave={() => setHover(0)}>
      {Array.from({ length: max }, (_, i) => i + 1).map(n => (
        <label key={n} onMouseEnter={() => setHover(n)}>
          <input type="radio" name="rating" value={n} checked={value === n} onChange={() => onChange(n)} className="sr-only" />
          <span aria-hidden="true">{n <= (hover || value) ? '★' : '☆'}</span>
          <span className="sr-only">{n} star{n > 1 ? 's' : ''}</span>
        </label>
      ))}
    </div>
  );
}

4. Build an accordion where only one panel is open.

Store the open index in the parent; use buttons with aria-expanded and aria-controls.

function Accordion({ items }) {
  const [open, setOpen] = useState(null);
  return items.map((item, i) => (
    <section key={item.id}>
      <h3>
        <button aria-expanded={open === i} aria-controls={`panel-${item.id}`} onClick={() => setOpen(open === i ? null : i)}>
          {item.title}
        </button>
      </h3>
      <div id={`panel-${item.id}`} hidden={open !== i}>{item.body}</div>
    </section>
  ));
}

5. Build a countdown timer with start, pause and reset.

Keep the remaining seconds in state and the interval id in a ref; clear the interval on pause, at zero and on unmount.

function Countdown({ seconds = 60 }) {
  const [left, setLeft] = useState(seconds);
  const [running, setRunning] = useState(false);

  useEffect(() => {
    if (!running) return;
    const id = setInterval(() => {
      setLeft(s => {
        if (s <= 1) { setRunning(false); return 0; }
        return s - 1;
      });
    }, 1000);
    return () => clearInterval(id);          // pause and unmount clean-up
  }, [running]);

  const mm = String(Math.floor(left / 60)).padStart(2, '0');
  const ss = String(left % 60).padStart(2, '0');
  return (
    <div>
      <p aria-live="polite">{mm}:{ss}</p>
      <button onClick={() => setRunning(r => !r)}>{running ? 'Pause' : 'Start'}</button>
      <button onClick={() => { setRunning(false); setLeft(seconds); }}>Reset</button>
    </div>
  );
}

6. Build tabs with keyboard support.

Selected index in state; arrow keys move between tabs, following the WAI-ARIA tabs pattern.

function Tabs({ tabs }) {
  const [active, setActive] = useState(0);
  const onKeyDown = e => {
    if (e.key === 'ArrowRight') setActive(a => (a + 1) % tabs.length);
    if (e.key === 'ArrowLeft') setActive(a => (a - 1 + tabs.length) % tabs.length);
  };
  return (
    <>
      <div role="tablist" onKeyDown={onKeyDown}>
        {tabs.map((t, i) => (
          <button key={t.id} role="tab" aria-selected={active === i} tabIndex={active === i ? 0 : -1} onClick={() => setActive(i)}>
            {t.label}
          </button>
        ))}
      </div>
      <div role="tabpanel">{tabs[active].content}</div>
    </>
  );
}

7. Build infinite scroll with IntersectionObserver.

Observe a sentinel at the bottom; when it appears, load the next page unless already loading or finished.

function Feed() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);
  const sentinel = useRef(null);

  useEffect(() => {
    if (!hasMore) return;
    setLoading(true);
    fetch(`/api/posts?page=${page}`).then(r => r.json()).then(data => {
      setItems(prev => [...prev, ...data.items]);
      setHasMore(data.hasMore);
    }).finally(() => setLoading(false));
  }, [page, hasMore]);

  useEffect(() => {
    const io = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting && !loading && hasMore) setPage(p => p + 1);
    }, { rootMargin: '200px' });
    if (sentinel.current) io.observe(sentinel.current);
    return () => io.disconnect();
  }, [loading, hasMore]);

  return (
    <>
      {items.map(p => <article key={p.id}>{p.title}</article>)}
      {loading && <p>Loading…</p>}
      <div ref={sentinel} />
    </>
  );
}

8. Build a reusable modal that closes on Escape and outside click.

Use the native <dialog> element for focus trapping and Escape handling, controlled by an open prop.

function Modal({ open, onClose, title, children }) {
  const ref = useRef(null);
  useEffect(() => {
    const dialog = ref.current;
    if (open && !dialog.open) dialog.showModal();
    if (!open && dialog.open) dialog.close();
  }, [open]);
  return (
    <dialog ref={ref} onClose={onClose} onClick={e => { if (e.target === ref.current) onClose(); }} aria-labelledby="modal-title">
      <h2 id="modal-title">{title}</h2>
      {children}
      <button onClick={onClose}>Close</button>
    </dialog>
  );
}

Remember

Key points

  • Functional state updates

    setX(prev => …) inside timers.

  • Clean up every effect

    Intervals, observers, requests.

  • Abort stale fetches

    Prevent out-of-order results.

  • Native elements first

    dialog, button, radio inputs.

Interview drill

Follow-up questions

What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.

  1. 1 How would you persist the todo list across reloads? Basic

    Initialise state lazily from localStorage and save in an effect when todos change (guarding JSON parsing errors).

  2. 2 How would you implement undo for delete? Intermediate

    Keep the last deleted item (or a history stack) and restore it within a few seconds via a toast action.

  3. 3 How do you build a drag-and-drop reorder? Advanced

    Use the HTML drag events or a library such as dnd-kit, storing the order in state and providing keyboard alternatives for accessibility.

  4. 4 How do you show a toast notification system? Intermediate

    A context provider holding a list of toasts, a portal that renders them, and timeouts to remove each one.

  5. 5 How would you virtualise a list of 10,000 rows? Advanced

    Render only visible rows with absolute positioning based on scrollTop and row height, or use TanStack Virtual.

  6. 6 How do you prevent double form submission? Basic

    Disable the submit button while a request is in flight and ignore submits when a pending flag is set.

Preparing with friends?

Share this page with your study group or placement batch.

Search LearnFrenzy

Esc

Quick links