Tic Tac Toe I

React20 minApolloMicrosoftAtlassian

Prompt

Do you know what Tic Tac Toe is? Your task is to create a Tic Tac Toe game.

The objective is to develop a program that enables two players to take turns playing on a 3x3 grid, with the first player being designated as "X" and the second player as "O". The winner is the player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row.

Requirements

  • Render a 3x3 "Tic Tac Toe" board.

  • Enable players to place 'X' and 'O' marks on the board.

  • Implement a restart button that resets the game to its initial state.

  • Display the game status, which includes:

    • Next player: X
    • Match draw
    • Winner: X
  • Ensure the styling closely resembles the one shown in the provided GIF.

Example

Playground (Prompt 1)

Hint 1

Consider a way to represent the game board's state. Using an array is a practical approach, with each element corresponding to a square on the board. This array can hold values like 'X', 'O', or null. 'X' and 'O' represent the respective player's move in a square, while null indicates an unselected square. As players make their moves, update this array to reflect the current state of the game.

Hint 2

The game needs to alternate turns between the two players, 'X' and 'O'. One way to do this is by tracking the number of moves made so far. Consider a function that counts the filled squares in your game state array. If the count of filled squares is even, it could be 'X's turn; if odd, then it's 'O's turn. This pattern ensures that the players alternate after each move. How can you integrate this counting logic into your game's state updates to determine and display whose turn it is next?

Solution (Prompt 1)

Explanation

I will walk you through an actual interview challenge asked at companies like Atlassian, Microsoft, Apollo and Google. We will go through the solution step by step, where candidates go wrong, common mistakes, and how interviewers judge your solution.

Tic Tac Toe looks simple on the surface, but there are a lot of moving parts: state management, derived values, component architecture, handling edge cases. Before writing any code, take a moment to read the prompt carefully and make sure you understand every requirement. For example, what should happen when a square is already filled? What does the game status say during different phases? If something is unclear, ask the interviewer. I have seen candidates start coding immediately and then realise halfway through that they misunderstood a requirement. With only 20 minutes on this prompt, there is no room to start over.

Now, let's start solving.

Component Architecture

Before we start writing any code, let's think about how we want to structure our components. I always recommend planning component architecture early, you might never get time later.

We will create three components:

  • Game — The smart parent. Holds the squares state and all game logic (whose turn it is, game status, restart). Think of it as the brain of our application.
  • Board — Takes squares as props and renders the 3x3 grid. It does not know anything about game logic, it just renders what it is told.
  • Square — A single square on the board. It is just a button.

Our App.js will simply import and render the Game component.

App.js

import React from 'react';
import './styles.css';
import Game from './components/Game';
function App() {
return <Game />;
}
export default App;

Common Pitfalls

  1. Candidates dump everything into App.js. They feel that creating separate components like Game, Board, Square will take a lot more time. In reality, it takes maybe 2 extra minutes and makes your code so much more organised. Interviewers notice this immediately.
  2. Some candidates create a Board component but put the state and game logic inside it. The Board should only be responsible for rendering the grid. If Board owns the state, it becomes less reusable and harder to extend later (for example when we add sorting controls or a restart button, they need access to the same state).

Now that we have a plan for our component structure, let's start building.

Thinking About State

The first thing I do when I see a problem like this is think about what data I need to track. What is the minimum state that fully describes the game at any moment?

For Tic Tac Toe, the answer is surprisingly simple, we just need one piece of state: an array of 9 values representing the 9 squares on the board. Each value can be null (empty), 'X', or 'O'. That's it.

Common Pitfalls

A very common mistake candidates make is to create multiple states: one for squares, one for nextPlayer, one for winner, one for status. This is incorrect. The next player, winner, and status can all be derived (calculated) from the squares state. This is a very important React concept called derived state. If something can be calculated from existing state, it should not be a separate state. Maintaining multiple states that depend on each other leads to bugs where they go out of sync.

We will start by creating a Game component which will hold our game state. We create a squares state initialised with Array(9).fill(null), this gives us an array of 9 null values representing 9 empty squares at the start. Array(9) creates an empty array with 9 slots, and .fill(null) fills every slot with null.

components/Game.js (Step 1)

import React, { useState } from 'react';
export default function Game() {
const [squares, setSquares] = useState(
Array(9).fill(null)
);
return null;
}

Interview Tip

Use Array(9).fill(null) instead of manually typing out [null, null, null, null, null, null, null, null, null]. It is cleaner and if the board size changes tomorrow, you only update one number.

Creating the Square Component

Let's start from the bottom up. Our Square component is the simplest. It is just a button that displays its value.

components/Square.js

import React from 'react';
export default function Square({ value, onClick }) {
return (
<button className="square" onClick={onClick}>
{value}
</button>
);
}

Notice that we are using a <button> HTML element and not a <div>. This is important for accessibility. Buttons are natively keyboard focusable, screen readers announce them as interactive elements, and they respond to both click and keyboard events (Enter, Space) out of the box. If you use a <div>, you would need to manually add tabIndex, role="button", and keyboard event handlers. Using the right semantic HTML element saves you all that work and shows the interviewer that you care about accessibility.

Creating the Board Component

Now, the Board component takes squares and an onClick handler as props and renders the 3x3 grid.

components/Board.js

import React from 'react';
import Square from './Square';
export default function Board({ squares, onClick }) {
return (
<div className="board">
{squares.map((square, i) => (
<Square
key={i}
value={square}
onClick={() => onClick(i)}
/>
))}
</div>
);
}

A few important things here:

We are using the map method to render 9 Square components from our squares array. Each Square gets the key prop (important for React's reconciliation), its value (which will be null, 'X', or 'O'), and an onClick handler.

Notice how we pass () => onClick(i) and not just onClick. We need to tell the parent which square was clicked, so we wrap it in an arrow function that passes the index i. This is a pattern you will use very often in React.

Common Pitfalls

Using array index as the key prop is generally not recommended because if items get reordered or deleted, React can get confused about which component is which. But in Tic Tac Toe, our squares never get reordered or removed, they only get filled in. So using the index as key is perfectly fine here. If an interviewer asks about it, you should be ready to explain why it is acceptable in this case.

Handling Square Clicks

Now let's go back to our Game component and add the click handling logic.

components/Game.js (Step 2)

import React, { useState } from 'react';
import Board from './Board';
export default function Game() {
const [squares, setSquares] = useState(
Array(9).fill(null)
);
function handleSquareSelect(square) {
if (squares[square]) return;
const squaresCopy = [...squares];
squaresCopy[square] = 'X';
setSquares(squaresCopy);
}
return (
<main className="game">
<Board
squares={squares}
onClick={handleSquareSelect}
/>
</main>
);
}

Let's break down the handleSquareSelect function:

  1. Guard clause: if (squares[square]) return; — If the square is already filled (has 'X' or 'O'), we do nothing and return. This prevents a player from overwriting an already played square.
  2. Immutable update: const squaresCopy = [...squares]; — We create a copy of the squares array using the spread operator. We never modify the original array directly.
  3. Update the copy: squaresCopy[square] = 'X'; — We set the clicked square's value on the copy.
  4. Set state: setSquares(squaresCopy); — We update the state with the new copy.

Common Pitfalls

This is a very critical mistake: many candidates directly mutate the state like this:

squares[square] = 'X';
setSquares(squares);

This will not work. React compares the old state reference with the new one to decide if it should re-render. If you mutate the same array and pass it back, React sees the same reference and thinks nothing changed, so it does not re-render. You must always create a new copy. This is one of the most fundamental React concepts and something interviewers specifically look for.

Interview Tip

I highly recommend using console.log during interviews. After implementing the click handler, log the squares state to verify it is updating correctly. A lot of the time the state does not get set as expected, and later on it causes issues that are hard to debug. It is better to keep checking step by step. Don't try to write all the code and test everything at once.

Calculating the Next Player

Right now, we hardcoded 'X' in the click handler. We need to alternate between 'X' and 'O'. Instead of creating a separate state for this, we will derive it from the squares state.

The logic is simple: count how many squares are filled. If the count is even, it is X's turn. If odd, it is O's turn. We use filter(Boolean) to count filled squares because null is falsy, and 'X' and 'O' are truthy.

function calculateNextPlayer(squares) {
return squares.filter(Boolean).length % 2 === 0
? 'X'
: 'O';
}

This is a clean, derived calculation. No extra state needed. Every time the component re-renders, we recalculate who the next player is based on the current squares state.

Game Status and Restart

We also need to display the game status ("Next player: X" or "Match draw") and provide a restart button.

function calculateStatus(squares, nextPlayer) {
return squares.every(Boolean)
? `Match draw`
: `Next player: ${nextPlayer}`;
}

The calculateStatus function checks if every square is filled using squares.every(Boolean). If all 9 squares are filled and there is no winner (we will add winner calculation in Tic Tac Toe II), it is a draw. Otherwise, it shows whose turn it is.

For restarting, we simply reset the state to Array(9).fill(null).

components/Game.js (Step 3)

import React, { useState } from 'react';
import Board from './Board';
function calculateNextPlayer(squares) {
return squares.filter(Boolean).length % 2 === 0
? 'X'
: 'O';
}
function calculateStatus(squares, nextPlayer) {
return squares.every(Boolean)
? `Match draw`
: `Next player: ${nextPlayer}`;
}
export default function Game() {
const [squares, setSquares] = useState(
Array(9).fill(null)
);
const nextPlayer = calculateNextPlayer(squares);
const status = calculateStatus(squares, nextPlayer);
function handleSquareSelect(square) {
if (squares[square]) return;
const squaresCopy = [...squares];
squaresCopy[square] = nextPlayer;
setSquares(squaresCopy);
}
function handleRestartClick() {
setSquares(Array(9).fill(null));
}
return (
<main className="game">
<p>{status}</p>
<Board
squares={squares}
onClick={handleSquareSelect}
/>
<button onClick={handleRestartClick}>restart</button>
</main>
);
}

Notice how calculateNextPlayer and calculateStatus are defined outside the Game component. They do not depend on any state or props directly, they take their inputs as function arguments. This is a good practice: if a function does not need access to component internals, define it outside the component. It makes the code more readable, testable, and avoids unnecessary recreation on every render.

Styling the Tic Tac Toe Board

Now, let's talk about styling. The most important part is the 3x3 grid layout.

:root {
--square-size: 48px;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
.game {
margin: 20px;
font-size: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.board {
display: grid;
grid-template: repeat(3, 1fr) / repeat(3, 1fr);
}
.square {
background: #fff;
border: 1px solid #999;
font-size: 24px;
font-weight: bold;
width: var(--square-size);
height: var(--square-size);
line-height: 40px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
}
.square:focus {
background: #ddd;
}

Let me explain the key CSS decisions:

As always, the first thing I do is add box-sizing: border-box; to make width and height calculations predictable.

For the board, I am using CSS Grid with grid-template: repeat(3, 1fr) / repeat(3, 1fr). This is the shorthand for grid-template-rows and grid-template-columns. It creates a perfect 3x3 grid in a single line. Some candidates use Flexbox with flex-wrap: wrap for this, which works but CSS Grid is much more natural for a grid layout. This is the kind of decision that shows the interviewer you understand when to use Grid vs Flexbox.

For the .game container, I am using Flexbox with flex-direction: column to stack the status text, board, and restart button vertically. Using align-items: center centers everything horizontally and gap: 16px adds consistent spacing.

For the .square styling, we use a CSS variable --square-size: 48px for both width and height to keep the squares perfectly sized. This is a nice practice, if you need to adjust the board size later, you only change one value. We set font-size: 24px and font-weight: bold so the X and O marks are clearly visible inside the squares. We reset padding: 0 on the button because browsers add default padding to buttons which would mess with our sizing. The text-align: center combined with line-height: 40px vertically and horizontally centers the text inside the square. Each square also gets a border: 1px solid #999 to create the grid lines.

Common Pitfalls

The margin-right: -1px; margin-top: -1px; on the squares is a subtle but important trick. Each square has a 1px border on all sides. When two squares sit next to each other, their borders combine to create a 2px border between them, which looks uneven compared to the 1px border on the outside edges. The negative margins pull the squares slightly together so the borders overlap, creating a uniform 1px border everywhere. Without this, your grid will look slightly off. It is a small detail but interviewers who have an eye for CSS will notice.

The .square:focus style gives a visual indicator when a square is focused via keyboard navigation. This is important for accessibility, it allows keyboard users to see which square they are about to select.

Communicate While You Code

Throughout the interview, it is very important for the candidate to express their thoughts and the important decisions they are making while writing code. Lots of candidates silently code the solution and don't talk much. It is very important for candidates to understand that coding is one part of your job as a software developer, explaining code and reasoning about code are another very important part of your job. The interviewer loves candidates who can talk through their solutions. So, please don't be silent, but don't over express too, talk about the important decisions for example:

  • I am representing the board as a flat array of 9 elements instead of a 2D array because it is simpler to work with
  • I have decided to derive the next player from the squares state instead of maintaining a separate state
  • I am using CSS Grid instead of Flexbox for the board because it is a natural fit for a grid layout

It gives more confidence to the interviewer that you have solid concepts and you can reason well and communicate well.

Interviewer Criteria

HTML/CSS

  • Does my layout accurately reflect the classic Tic Tac Toe design?

  • Are my CSS class names both self-explanatory and meaningful, enhancing readability?

  • Did I use the button HTML element to build the squares in my Tic Tac Toe game, ensuring good semantic structure and accessibility?

  • How effortlessly was I able to create the Tic Tac Toe grid layout using CSS?

JavaScript

  • Is the game logic for determining the next player both robust and efficient?

  • How effectively did I manage the game's state, particularly in terms of updating and rendering the squares?

  • Do my variable and function names across the codebase enhance the readability and maintainability of my code?

  • How effectively did I handle edge cases, like clicking an already filled square?

React

  • Did I efficiently manage the game's state by deriving all necessary values (like the next player and game status) from a single squares state, instead of maintaining multiple states?

  • Have I used React hooks, such as useState, optimally to manage the game's state?

  • Did I ensure proper use of the key prop in React lists, especially in rendering the game squares, to optimize performance?

  • Are my components, especially <Square /> and <Board /> , designed for maximum reusability and clarity?

  • Have I ensured that the state of the Tic Tac Toe game, particularly the squares array, is not directly mutated, and that I'm manipulating the array in an immutable way?

Component Architecture

  • Have I structured my components (Game, Board, Square) to maintain a clear and logical hierarchy?

  • Are the component names and their respective functionalities clear and easy to understand?

  • Did I effectively separate the game logic from the UI components to maintain clean code architecture?

Time Checkpoints

  • 10:00 AM

    Interview starts

  • 10:03 AM

    Interviewer introduces the prompt

  • 10:05 AM

    Candidate reviews the prompt, seeks clarification as needed, and initiates coding

  • 10:10 AM

    Game, Square, and Board components barebones built

  • 10:12 AM

    Squares state initialized and <Square/> rendered using squares state and map method

  • 10:20 AM

    Styling of Tic Tac Toe completed

  • 10:22 AM

    Adjusted board to render empty squares with Array.fill

  • 10:27 AM

    Square selection logic implemented

  • 10:30 AM

    Logic to calculate next player implemented

  • 10:32 AM

    Game status calculated and rendered

  • 10:34 AM

    Restart game button and functionality added

  • 10:36 AM

    Winner calculation logic added

  • 10:40 AM

    sendRequest function imported and used

  • 10:45 AM

    Board updating logic moved to sendRequest function

  • 10:50 AM

    Error and loading state added

  • 11:00 AM

    Interview ends