HomeAbout Me

React Hook Section 6: Tic Tac Toe

By Daniel Nguyen
Published in React JS
May 16, 2025
2 min read
React Hook Section 6: Tic Tac Toe

Tic Tac Toe

This exercise is just to give you more practice with useState and useEffect. For the most part, this is just review, but there is one thing we’ll be doing in this exercise we haven’t done yet with useState and that is, it can actually accept a function. For example:

const [count, setCount] = useState(0)
// then in a click event handler or something:
setCount(count + 1)
// but this is the exact same thing:
setCount(previousCount => previousCount + 1)

Because there are two ways to do the same thing, it’s nice to know why/when you’d use one over the other. Here’s the “rule”:

If your new value for state is calculated based on the previous value of state, use the function form. Otherwise, either works fine.

For a deeper dive on this, read useState lazy initialization and function updates.

This one’s a bit tougher than some of the other exercises, so make sure you’re well hydrated and do a bit of stretching before starting this.

setState callback

👨‍💼 Our users want to play tic-tac-toe.

If you've gone through React's official tutorial, this was originally lifted from that.

You’re going to need some managed state and some derived state. Remember from exercise 1:

  • Managed State: State that you need to explicitly manage
  • Derived State: State that you can calculate based on other state

squares is the managed state and it’s the state of the board in a single-dimensional array:

[
'X', 'O', 'X',
'X', 'O', 'O',
'X', 'X', 'O'
]

This will start out as an empty array because it’s the start of the game.

nextValue will be either the string X or O and is derived state which you can determine based on the value of squares. We can determine whose turn it is based on how many “X” and “O” squares there are. We’ve written this out for you in a calculateNextValue function in the tic-tac-toe-utils.tsx file.

winner will be either the string X or O and is derived state which can also be determined based on the value of squares and we’ve provided a calculateWinner function you can use to get that value.

If you want to try this exercise on beast mode then you can ignore calculateNextValue and calculateWinner and write your own version of those utilities.

Another important thing you’ll need to do for this step of the exercise is to use the callback version of setState to ensure that the state is updated correctly. This is because the state is updated based on the previous state and you want to make sure that you’re always using the most up-to-date state.

setCount(currentCount => {
return currentCount + 1
})

Finally, something you need to know about state in React is it’s important that you not mutate state directly. So instead of setting squares[0] = 'X' you will need to make a copy of the array with the modifications, for example, using with:

const newSquares = squares.with(index, 'X')

The emoji should guide you well! Enjoy the exercise!

Preserve State in localStorage

👨‍💼 Our customers want to be able to close the tab in the middle of a game and then resume the game later. Can you store the game’s state in localStorage?

I think you can! You’re going to need to use useEffect to coordinate things with localStorage. Luckily for us, localStorage is synchronous, so initializing our state from localStorage is straightforward (you should definitely use the callback form of useState for initialization though!).

For keeping the squares up-to-date in localStorage, you’ll want to use useEffect with a dependency array that includes the squares.

📜 If you need to learn a bit about the localStorage API, you can check out the MDN documentation.

🚨 Note this exercise depends on `localStorage` and so the tests could interfer with your work by changing the `localStorage` you're working with.# Add Game History Feature

👨‍💼 Sometimes users are playing tic-tac-toe against their children and their kids want to change their move when they lose 😆 So we’re going to add a feature that allows users to go backward through the game and change moves.

As usual, you can check

the solution
to check what the final experience should be like.

🧝‍♂️ I made a couple changes you can

check here
if you’d like. I mostly just refactored things a bit to get it ready for your work. Should be a fun feature to add! Enjoy!

import { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import {
calculateNextValue,
calculateStatus,
calculateWinner,
isValidGameState,
type GameState,
type Squares,
} from '#shared/tic-tac-toe-utils'
function Board({
squares,
onClick,
}: {
squares: Squares
onClick: (index: number) => void
}) {
function renderSquare(i: number) {
const value = squares[i]
const label = value ? `square ${i}, ${value}` : `square ${i} empty`
return (
<button className="square" onClick={() => onClick(i)} aria-label={label}>
{squares[i]}
</button>
)
}
return (
<div>
<div className="board-row">
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div className="board-row">
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div className="board-row">
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</div>
)
}
const defaultState: GameState = {
history: [Array(9).fill(null)],
currentStep: 0,
}
const localStorageKey = 'tic-tac-toe'
function App() {
const [state, setState] = useState<GameState>(() => {
let localStorageValue
try {
localStorageValue = JSON.parse(
window.localStorage.getItem(localStorageKey) ?? 'null',
)
} catch {
// something is wrong in localStorage, so don't use it
}
return isValidGameState(localStorageValue)
? localStorageValue
: defaultState
})
const currentSquares = state.history[state.currentStep]
const winner = calculateWinner(currentSquares)
const nextValue = calculateNextValue(currentSquares)
const status = calculateStatus(winner, currentSquares, nextValue)
useEffect(() => {
window.localStorage.setItem(localStorageKey, JSON.stringify(state))
}, [state])
function selectSquare(index: number) {
if (winner || currentSquares[index]) return
setState(previousState => {
const { currentStep, history } = previousState
const newHistory = history.slice(0, currentStep + 1)
const squares = history[currentStep].with(index, nextValue)
return {
history: [...newHistory, squares],
currentStep: newHistory.length,
}
})
}
function restart() {
setState(defaultState)
}
const moves = state.history.map((_stepSquares, step) => {
const desc = step ? `Go to move number ${step}` : 'Go to game start'
const isCurrentStep = step === state.currentStep
return (
<li key={step}>
<button
onClick={() =>
setState(previousState => ({ ...previousState, currentStep: step }))
}
disabled={isCurrentStep}
>
{desc} {isCurrentStep ? '(current)' : null}
</button>
</li>
)
})
return (
<div className="game">
<div className="game-board">
<Board onClick={selectSquare} squares={currentSquares} />
<button className="restart" onClick={restart}>
restart
</button>
</div>
<div className="game-info">
<div aria-live="polite">{status}</div>
<ol>{moves}</ol>
</div>
</div>
)
}
const rootEl = document.createElement('div')
document.body.append(rootEl)
createRoot(rootEl).render(<App />)
.game {
font:
14px 'Century Gothic',
Futura,
sans-serif;
margin: 20px;
min-height: 260px;
}
.game ol,
.game ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.restart {
margin-top: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
min-width: 190px;
}

Tags

#ReactHooks

Share

Previous Article
React Hook Section 5: Unique IDs

Table Of Contents

1
Tic Tac Toe
2
setState callback
3
Preserve State in localStorage

Related Posts

React Hook Section 5: Unique IDs
May 15, 2025
1 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media