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.
👨💼 Our users want to play tic-tac-toe.
You’re going to need some managed state and some derived state. Remember from exercise 1:
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!
👨💼 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.
👨💼 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
🧝♂️ I made a couple changes you can
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: SquaresonClick: (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 localStorageValuetry {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]) returnsetState(previousState => {const { currentStep, history } = previousStateconst 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.currentStepreturn (<li key={step}><buttononClick={() =>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;}
Quick Links
Legal Stuff
Social Media