🎮 Building Tic Tac Toe in React: Mastering useState
, useEffect
, and Game State Persistence
Want to level up your React skills with a hands-on project? Let’s build a classic Tic Tac Toe game—but with a twist! We’ll not only manage the game board and handle turns, but also:
useState
localStorage
Let’s dive in! 🏊♂️
useState
Functional UpdatesReact’s useState
lets you update state either directly or via a callback:
const [count, setCount] = useState(0)// Direct updatesetCount(count + 1)// Functional updatesetCount(prevCount => prevCount + 1)
Rule of Thumb: If your new state depends on the previous value, always use the function form. It ensures you’re working with the most up-to-date state—especially helpful when updates are batched or asynchronous.
Let’s outline the core concepts you’ll implement:
squares
: A flat array of 9 elements representing the game board.
Example:
['X', 'O', 'X', 'X', 'O', 'O', 'X', 'X', 'O']
nextValue
: Calculated based on how many “X”s and “O”s are on the board. (We’ve provided a calculateNextValue()
utility for this!)winner
: Determined by checking win conditions. (calculateWinner()
handles this logic.)Note: Derived state is calculated on the fly—you don’t store it in useState
.
You’ll update the squares
array when a user clicks a square—but never mutate state directly!
❌ Don’t do:
squares[0] = 'X'
✅ Do this instead (using Array.prototype.with()
):
const newSquares = squares.with(index, 'X')
This ensures immutability, which is essential for React to detect changes correctly.
localStorage
Want players to resume the game even after closing the browser tab? Store game state in localStorage
!
const [squares, setSquares] = useState(() => {const stored = localStorage.getItem('tic-tac-toe:squares')return stored ? JSON.parse(stored) : Array(9).fill(null)})
useEffect
useEffect(() => {localStorage.setItem('tic-tac-toe:squares', JSON.stringify(squares))}, [squares])
Now your app is stateful and persistent!
Kids hate losing? 😅 Let’s let them rewind the game and change their moves.
Instead of just tracking the current board state (squares
), you track a history of all board states.
const [history, setHistory] = useState([Array(9).fill(null)])const [currentStep, setCurrentStep] = useState(0)const currentSquares = history[currentStep]
When a player makes a move:
currentStep
.setHistory(prev => [...prev.slice(0, currentStep + 1), newSquares])setCurrentStep(prev => prev + 1)
Add UI buttons to jump to any past move:
<button onClick={() => setCurrentStep(index)}>Go to move #{index}</button>
localStorage
🧪 Be cautious! If you’re writing automated tests or switching between development and test modes, your localStorage
values can conflict. Clear the storage between sessions or use unique keys to isolate environments.
Here’s what you’ll master in this exercise:
Feature | Concept |
---|---|
Game board (squares ) | Managed state |
Player turn / winner | Derived state |
Local game persistence | useEffect + localStorage |
Undoable move history | Functional setState and immutability |
Performance & correctness | useState callback form |
This isn’t just about building a simple game—it’s a deep dive into how React state really works, and how to structure real-world, user-friendly features.
Happy coding! 🧠💡 Let me know if you’d like a working code template for this.
Quick Links
Legal Stuff
Social Media