Problem Understanding
Let me walk you through this problem properly. Tic-Tac-Toe looks simple — it's a 3x3 grid where two players take turns. But it's a great interview question because it tests how you model state, structure components, and derive values cleanly in React.
Before writing a single line of code, ask these clarifying questions: Does clicking a filled square do anything? What happens after a win — can players keep clicking? Is there a score tracker across games, or just a single game with a restart? For this challenge, we keep it to a single game.
💡 Interview Tip: Taking 2 minutes to ask clarifying questions shows the interviewer you think before you code. It's one of the biggest differentiators between junior and senior candidates.
Component Architecture
Plan your component structure before writing any code. For Tic-Tac-Toe, the ideal breakdown is three components:
- Game — The brain. Owns all state and game logic (whose turn it is, winner calculation, restart). This is a smart component.
- Board — A dumb rendering component. Receives squares and onClick as props, renders the 3x3 grid. Knows nothing about game logic.
- Square — A single button. Receives value and onClick, renders one cell.
⚠️ Common Mistake: Dumping everything into App.js. Candidates think creating separate components wastes time — in reality it takes 2 extra minutes and makes your code dramatically more readable. Interviewers notice this immediately.
State Design
Here is the key insight: you only need ONE piece of state — an array of 9 values representing the board squares.
const [squares, setSquares] = useState(Array(9).fill(null));
Everything else — whose turn it is, the winner, the game status — can be derived from this array. This is called derived state, and it's a critical React concept.
⚠️ Common Mistake: Creating separate state for currentPlayer, winner, and isGameOver. These go out of sync and create bugs. If something can be calculated from existing state, it should never be its own state variable.
// Derive current player — don't store it
const nextPlayer = squares.filter(Boolean).length % 2 === 0 ? 'X' : 'O';
Step-by-Step Implementation
Square Component
function Square({ value, onClick }) {
return (
<button className="square" onClick={onClick}>
{value}
</button>
);
}
💡 Interview Tip: Always use a <button> element, not a <div>. Buttons are keyboard focusable, accessible, and handle Enter/Space key presses automatically. Using a div requires manually adding tabIndex, role="button", and keyboard handlers.
Board Component
function Board({ squares, onClick }) {
return (
<div className="board">
{squares.map((square, i) => (
<Square key={i} value={square} onClick={() => onClick(i)} />
))}
</div>
);
}
Game Component
export default function Game() {
const [squares, setSquares] = useState(Array(9).fill(null));
const nextPlayer = squares.filter(Boolean).length % 2 === 0 ? 'X' : 'O';
const winner = calculateWinner(squares);
const status = winner
? \`Winner: \${winner}\`
: squares.every(Boolean) ? 'Match draw'
: \`Next player: \${nextPlayer}\`;
function handleClick(i) {
if (squares[i] || winner) return;
const next = [...squares];
next[i] = nextPlayer;
setSquares(next);
}
return (
<main className="game">
<p>{status}</p>
<Board squares={squares} onClick={handleClick} />
<button onClick={() => setSquares(Array(9).fill(null))}>Restart</button>
</main>
);
}
Core Logic
The winner calculation checks all 8 possible winning lines — 3 rows, 3 columns, 2 diagonals:
function calculateWinner(squares) {
const lines = [
[0,1,2],[3,4,5],[6,7,8], // rows
[0,3,6],[1,4,7],[2,5,8], // columns
[0,4,8],[2,4,6], // diagonals
];
for (const [a,b,c] of lines) {
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
Edge Cases
- Clicking a filled square: Guard with
if (squares[i] || winner) return;
- Moves after win: The same guard stops all moves once a winner exists
- Draw detection:
squares.every(Boolean) returns true when all 9 squares are filled
Styling Approach
Use CSS Grid for the board — it's the natural choice for a 2D layout:
.board {
display: grid;
grid-template-columns: repeat(3, 48px);
grid-template-rows: repeat(3, 48px);
}
.square {
border: 1px solid #999;
background: #fff;
font-size: 24px;
font-weight: bold;
margin: -1px 0 0 -1px; /* overlap borders to avoid double-width lines */
}
💡 Interview Tip: The negative margin trick (margin: -1px 0 0 -1px) makes all borders exactly 1px by overlapping adjacent borders. Without it, you get 2px borders between squares and 1px borders on edges — looks uneven.
Common Pitfalls
- State mutation: Never do
squares[i] = 'X' directly. Always spread: const next = [...squares]
- Too much state: Storing currentPlayer, isGameOver, winner as separate states leads to sync bugs
- Using divs instead of buttons: Fails accessibility checks and misses free keyboard support
How to Communicate During the Interview
Talk through your decisions as you code. Specifically mention:
- "I'm representing the board as a flat array of 9 elements — simpler than a 2D array and maps directly to indices"
- "I'm deriving nextPlayer from the squares array instead of adding a separate state — this eliminates an entire class of sync bugs"
- "I'm using CSS Grid here because it's the natural tool for a grid layout — Flexbox would work but Grid is more semantic"
- "I'm using a button element not a div because it gives us keyboard accessibility for free"