Skip to the solution
MediumMachine Coding10 min read

Build a Quiz App

208 views

The problem

Question presented to candidate: "Build a short multiple-choice quiz. Show one question at a time with four options, a Next button and a countdown for each question. If time runs out, record the question as unanswered and move on. At the end, show the score and a review of every question with the correct answer, and let the user restart."

Requirements:

  • Intro screen, then questions one at a time with "Question N of M" and a progress bar, then a review screen.
  • Select an option (mouse or keys 1-4), then Next (or Enter); Next is disabled until something is selected; the last button says Finish.
  • A 10-second timer per question that does not drift and auto-advances with the answer recorded as "time ran out".
  • Review: score as X / N and a percentage, each question marked right or wrong with your answer and the correct one; Restart resets everything.
  • Focus moves to the new question heading so screen-reader users hear each question.

What a strong answer should cover:

  • Modelling the flow as a state machine (intro, question, review) with the index, the current selection and an answers array.
  • Score as derived data computed from answers, never stored separately.
  • A timer measured against a deadline, cleared when the question changes or the component unmounts, and protected against a late tick skipping the next question.
  • Global keyboard shortcuts added and removed with the lifecycle, without hijacking Enter on buttons.
  • Accessibility: native radios in a fieldset, role="timer", focus management between questions.

Clarifying questions expected:

  • "Should answers be revealed after each question or only at the end?"
  • "Can users go back to change an answer?"
  • "Are questions loaded from an API, and should their order or options be shuffled?"

Code / implementation expected: Yes. A working quiz with timer, keyboard support, review and restart.

  • React
  • Quiz
  • State

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

10 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 { useEffect, useReducer, useRef, useState } from "react";
import { QUESTIONS, SECONDS_PER_QUESTION } from "./src/data/questions";
import { initialState, quizReducer } from "./src/utils/quizReducer";
import Review from "./src/components/Review";

export default function App() {
  const [state, dispatch] = useReducer(quizReducer, initialState);
  const [now, setNow] = useState(() => Date.now());
  const heading = useRef(null);
  const { phase, index, selected } = state;
  const q = QUESTIONS[index];

  // Timer: tick a clock, and time out against a fixed deadline (no drift).
  useEffect(() => {
    if (phase !== "question") return;
    const id = setInterval(() => {
      const t = Date.now();
      setNow(t);
      if (t >= state.deadline) dispatch({ type: "TIMEOUT", index, at: t });
    }, 200);
    return () => clearInterval(id); // new question, review or unmount: stop
  }, [phase, index, state.deadline]);

  // Keyboard: 1-4 pick an option, Enter goes on. Only while a question shows.
  useEffect(() => {
    if (phase !== "question") return;
    const onKey = (e) => {
      // let buttons handle their own Enter
      if (e.target.closest && e.target.closest("button")) return;
      const n = Number(e.key);
      if (n >= 1 && n <= q.options.length)
        dispatch({ type: "SELECT", option: n - 1 });
      if (e.key === "Enter") dispatch({ type: "NEXT", at: Date.now() });
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [phase, q]);

  // Move focus to the new heading so screen readers announce the question.
  useEffect(() => {
    if (phase === "question") heading.current?.focus();
  }, [phase, index]);

  if (phase === "intro") {
    return (
      <main className="app">
        <section className="card">
          <h1>Frontend quiz</h1>
          <p>
            {QUESTIONS.length} questions, {SECONDS_PER_QUESTION} seconds each.
            Keys 1-4 pick an answer, Enter goes on.
          </p>
          <button
            data-testid="start"
            onClick={() => {
              setNow(Date.now());
              dispatch({ type: "START", at: Date.now() });
            }}
          >
            Start quiz
          </button>
        </section>
      </main>
    );
  }

  if (phase === "review") {
    return (
      <main className="app">
        <Review
          answers={state.answers}
          onRestart={() => {
            setNow(Date.now());
            dispatch({ type: "RESTART", at: Date.now() });
          }}
        />
      </main>
    );
  }

  const left = Math.max(
    0,
    Math.min(SECONDS_PER_QUESTION, Math.ceil((state.deadline - now) / 1000)),
  );
  return (
    <main className="app">
      <section className="card" aria-labelledby="q-title">
        <div className="top">
          <span data-testid="progress">
            Question {index + 1} of {QUESTIONS.length}
          </span>
          <span
            className={"timer" + (left <= 3 ? " low" : "")}
            role="timer"
            data-testid="timer"
          >
            {left}s
          </span>
        </div>
        <progress
          max={QUESTIONS.length}
          value={index}
          aria-label="Quiz progress"
        />
        <h2 id="q-title" tabIndex={-1} ref={heading} data-testid="heading">
          {q.prompt}
        </h2>
        <fieldset className="options">
          <legend className="sr-only">Choose one answer</legend>
          {q.options.map((opt, i) => (
            <label
              key={opt}
              className={"option" + (selected === i ? " chosen" : "")}
            >
              <input
                type="radio"
                name={q.id}
                checked={selected === i}
                onChange={() => dispatch({ type: "SELECT", option: i })}
              />
              <kbd aria-hidden="true">{i + 1}</kbd> {opt}
            </label>
          ))}
        </fieldset>
        <button
          data-testid="next"
          disabled={selected === null}
          onClick={() => dispatch({ type: "NEXT", at: Date.now() })}
        >
          {index === QUESTIONS.length - 1 ? "Finish" : "Next"}
        </button>
      </section>
    </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 69 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 nextMedium

Build a Collapsible Sidebar Menu

Continue