Skip to the solution
HardFrontend11 min read

Build Nested Comments with Collapsible Threads & Voting

281 views

The problem

Question presented to candidate: "Take a basic nested comments widget and make it feel like Reddit or Hacker News. People can upvote or downvote any comment and see the score change immediately, even though the server is slow and sometimes fails. The thread can be sorted by Best or New, clicking the vertical thread line collapses that branch, and very deep threads stop indenting and show a 'Continue this thread' link instead."

Requirements:

  • Up and down vote buttons per comment with aria-pressed; clicking the same arrow again removes the vote; switching arrows replaces it.
  • Optimistic update: the score changes at once, a simulated API (300 ms) confirms or fails; on failure the vote rolls back to the last confirmed value with a message.
  • Out-of-order responses must not corrupt state: only the newest request for a comment may roll back.
  • Sort Best (server score, ties older first) or New (newest first), applied at every level, without reshuffling rows when you vote.
  • Collapse a thread from its [-] toggle or by clicking its thread line; collapsed comments show how many replies are hidden.
  • Depth limit: below depth 3, show "Continue this thread (N more)", which opens that subtree on its own with a Back button and moves focus to the heading.

What a strong answer should cover:

  • Separating server data (score) from the user's own vote, and showing score + myVote.
  • Optimistic update with rollback to a confirmed value, plus a per-comment sequence number so a stale failure does not undo a newer vote.
  • Sorting as derived data per level, and why ranking on the server score avoids rows jumping under the cursor.
  • Lifting collapse state out of the components (a Set of ids) so it survives re-sorting and the "continue thread" view.
  • Depth limiting and focus management when switching views; accessible toggles and thread lines.

Clarifying questions expected:

  • "Can users vote on their own comments? Is a down vote allowed?"
  • "Should a new vote re-rank the thread immediately, or only on reload?"
  • "What should the user see when a vote fails: silent rollback or a message?"

Code / implementation expected: Yes. The vote flow with rollback and the stale-response guard is the core; sorting, collapse and the depth limit complete it.

  • React
  • Nested Comments
  • Tree
  • Optimistic

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

11 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, useRef, useState } from "react";
import Comment from "./src/components/Comment";
import {
  initialComments,
  sortLevel,
  findComment,
  sendVote,
} from "./src/utils/thread";

export default function App() {
  const [comments] = useState(initialComments);
  const [sort, setSort] = useState("best");
  // id -> -1 | 0 | 1, shown optimistically
  const [votes, setVotes] = useState({});
  const [collapsed, setCollapsed] = useState(() => new Set());
  // "continue this thread" root, or null
  const [threadId, setThreadId] = useState(null);
  const [simulateFailure, setSimulateFailure] = useState(false);
  const [status, setStatus] = useState("");
  const confirmed = useRef({}); // id -> last vote the server accepted
  const latest = useRef({}); // id -> sequence number of the newest request
  const headingRef = useRef(null);
  const focusHeading = useRef(false);

  // After switching between the full list and
  // one thread, move focus to the heading.
  useEffect(() => {
    if (!focusHeading.current) return;
    focusHeading.current = false;
    headingRef.current.focus();
  });

  function openThread(id) {
    focusHeading.current = true;
    setThreadId(id);
  }

  function vote(comment, dir) {
    const id = comment.id;
    // same arrow again = remove my vote
    const next = (votes[id] || 0) === dir ? 0 : dir;
    setVotes((v) => ({ ...v, [id]: next })); // 1. optimistic: show it now
    const seq = (latest.current[id] || 0) + 1;
    latest.current[id] = seq;
    sendVote(id, next, simulateFailure).then(
      () => {
        // 2a. confirmed
        if (latest.current[id] === seq) confirmed.current[id] = next;
      },
      () => {
        // 2b. failed: roll back, unless a newer click superseded this one
        if (latest.current[id] !== seq) return;
        setVotes((v) => ({ ...v, [id]: confirmed.current[id] || 0 }));
        setStatus(
          "Could not save your vote on the comment by " +
            comment.author +
            ". It was restored.",
        );
      },
    );
  }

  function toggleCollapsed(id) {
    setCollapsed((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

  const ctx = { votes, collapsed, sort, vote, toggleCollapsed, openThread };
  const root = threadId
    ? [findComment(comments, threadId)]
    : sortLevel(comments, sort);

  return (
    <div className="app">
      <div className="bar">
        <h1 ref={headingRef} tabIndex={-1}>
          {threadId ? "Thread" : "Discussion"}
        </h1>
        <label>
          Sort{" "}
          <select
            data-testid="sort"
            value={sort}
            onChange={(e) => setSort(e.target.value)}
          >
            <option value="best">Best</option>
            <option value="new">New</option>
          </select>
        </label>
      </div>
      <label className="fail">
        <input
          type="checkbox"
          data-testid="fail"
          checked={simulateFailure}
          onChange={(e) => setSimulateFailure(e.target.checked)}
        />{" "}
        Simulate vote failures
      </label>
      {threadId && (
        <button
          className="back"
          data-testid="back"
          onClick={() => openThread(null)}
        >
          ← Back to all comments
        </button>
      )}
      <ul className="thread">
        {root.map((c) => (
          <Comment key={c.id} comment={c} depth={0} ctx={ctx} />
        ))}
      </ul>
      <p className="status" role="status" data-testid="status">
        {status}
      </p>
    </div>
  );
}

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 101 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 Live Stock Ticker with throttled updates

Continue