From 040e73a83df8f8932b387596990ed76e72488574 Mon Sep 17 00:00:00 2001 From: Danilo Britto Date: Fri, 1 Nov 2024 22:39:52 -0500 Subject: [PATCH] chore(docs): update docs content --- docs/guides/how-to-reset-state.md | 78 +- docs/guides/testing.md | 6 +- docs/guides/tutorial-tic-tac-toe.md | 1433 +++++++++++++++++++++++++++ 3 files changed, 1444 insertions(+), 73 deletions(-) create mode 100644 docs/guides/tutorial-tic-tac-toe.md diff --git a/docs/guides/how-to-reset-state.md b/docs/guides/how-to-reset-state.md index a1fde74f06..13f780c934 100644 --- a/docs/guides/how-to-reset-state.md +++ b/docs/guides/how-to-reset-state.md @@ -44,8 +44,8 @@ const useSlice = create()((set, get) => ({ Resetting multiple stores at once ```ts -import { create as _create } from 'zustand' -import type { StateCreator } from 'zustand' +import type * as zustand from 'zustand' +import { create: actualCreate } from 'zustand' const storeResetFns = new Set<() => void>() @@ -55,80 +55,16 @@ const resetAllStores = () => { }) } -export const create = (() => { - return (stateCreator: StateCreator) => { - const store = _create(stateCreator) - const initialState = store.getState() +export const create = (() => { + return (stateCreator: zustand.StateCreator) => { + const store = actualCreate(stateCreator) + const initialState = store.getInitialState() storeResetFns.add(() => { store.setState(initialState, true) }) return store } -}) as typeof _create -``` - -Resetting bound store using Slices pattern - -```ts -import create from 'zustand' -import type { StateCreator } from 'zustand' - -const sliceResetFns = new Set<() => void>() - -export const resetAllSlices = () => { - sliceResetFns.forEach((resetFn) => { - resetFn() - }) -} - -const initialBearState = { bears: 0 } - -interface BearSlice { - bears: number - addBear: () => void - eatFish: () => void -} - -const createBearSlice: StateCreator< - BearSlice & FishSlice, - [], - [], - BearSlice -> = (set) => { - sliceResetFns.add(() => set(initialBearState)) - return { - ...initialBearState, - addBear: () => set((state) => ({ bears: state.bears + 1 })), - eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), - } -} - -const initialStateFish = { fishes: 0 } - -interface FishSlice { - fishes: number - addFish: () => void -} - -const createFishSlice: StateCreator< - BearSlice & FishSlice, - [], - [], - FishSlice -> = (set) => { - sliceResetFns.add(() => set(initialStateFish)) - return { - ...initialStateFish, - addFish: () => set((state) => ({ fishes: state.fishes + 1 })), - } -} - -const useBoundStore = create()((...a) => ({ - ...createBearSlice(...a), - ...createFishSlice(...a), -})) - -export default useBoundStore +}) as typeof zustand.create ``` ## CodeSandbox Demo diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 5912b73f12..13f023ca02 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -83,8 +83,9 @@ In the next steps we are going to setup our Jest environment in order to mock Zu ```ts // __mocks__/zustand.ts -import * as zustand from 'zustand' import { act } from '@testing-library/react' +import type * as zustand from 'zustand' +export * from 'zustand' const { create: actualCreate, createStore: actualCreateStore } = jest.requireActual('zustand') @@ -172,8 +173,9 @@ In the next steps we are going to setup our Vitest environment in order to mock ```ts // __mocks__/zustand.ts -import * as zustand from 'zustand' import { act } from '@testing-library/react' +import type * as zustand from 'zustand' +export * from 'zustand' const { create: actualCreate, createStore: actualCreateStore } = await vi.importActual('zustand') diff --git a/docs/guides/tutorial-tic-tac-toe.md b/docs/guides/tutorial-tic-tac-toe.md new file mode 100644 index 0000000000..d6a86d61e8 --- /dev/null +++ b/docs/guides/tutorial-tic-tac-toe.md @@ -0,0 +1,1433 @@ +--- +title: 'Tutorial: Tic-Tac-Toe' +description: Building a game +nav: 0 +--- + +# Tutorial: Tic-Tac-Toe + +## Building a game + +You will build a small tic-tac-toe game during this tutorial. This tutorial does assume existing +React knowledge. The techniques you'll learn in the tutorial are fundamental to building any React +app, and fully understanding it will give you a deep understanding of React and Zustand. + +> [!NOTE] +> This tutorial is crafted for those who learn best through hands-on experience and want to swiftly +> create something tangible. It draws inspiration from React's tic-tac-toe tutorial. + +The tutorial is divided into several sections: + +- Setup for the tutorial will give you a starting point to follow the tutorial. +- Overview will teach you the fundamentals of React: components, props, and state. +- Completing the game will teach you the most common techniques in React development. +- Adding time travel will give you a deeper insight into the unique strengths of React. + +### What are you building? + +In this tutorial, you'll build an interactive tic-tac-toe game with React and Zustand. + +You can see what it will look like when you're finished here: + +```jsx +import { create } from 'zustand' +import { combine } from 'zustand/middleware' + +const useGameStore = create( + combine( + { + history: [Array(9).fill(null)], + currentMove: 0, + }, + (set, get) => { + return { + setHistory: (nextHistory) => { + set((state) => ({ + history: + typeof nextHistory === 'function' + ? nextHistory(state.history) + : nextHistory, + })) + }, + setCurrentMove: (nextCurrentMove) => { + set((state) => ({ + currentMove: + typeof nextCurrentMove === 'function' + ? nextCurrentMove(state.currentMove) + : nextCurrentMove, + })) + }, + } + }, + ), +) + +function Square({ value, onSquareClick }) { + return ( + + ) +} + +function Board({ xIsNext, squares, onPlay }) { + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + onPlay(nextSquares) + } + + return ( + <> +
{status}
+
+ {squares.map((_, i) => ( + handleClick(i)} + /> + ))} +
+ + ) +} + +export default function Game() { + const { history, setHistory, currentMove, setCurrentMove } = useGameStore() + const xIsNext = currentMove % 2 === 0 + const currentSquares = history[currentMove] + + function handlePlay(nextSquares) { + const nextHistory = [...history.slice(0, currentMove + 1), nextSquares] + setHistory(nextHistory) + setCurrentMove(nextHistory.length - 1) + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove) + } + + return ( +
+
+ +
+
+
    + {history.map((_, historyIndex) => { + const description = + historyIndex > 0 + ? `Go to move #${historyIndex}` + : 'Go to game start' + + return ( +
  1. + +
  2. + ) + })} +
+
+
+ ) +} + +function calculateWinner(squares) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i] + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a] + } + } + + return null +} + +function calculateTurns(squares) { + return squares.filter((square) => !square).length +} + +function calculateStatus(winner, turns, player) { + if (!winner && !turns) return 'Draw' + if (winner) return `Winner ${winner}` + return `Next player: ${player}` +} +``` + +### Building the board + +Let's start by creating the `Square` component, which will be a building block for our `Board` +component. This component will represent each square in our game. + +The `Square` component should take `value` and `onSquareClick` as props. It should return a +` + ) +} +``` + +Let's move on to creating the Board component, which will consist of 9 squares arranged in a grid. +This component will serve as the main playing area for our game. + +The `Board` component should return a `
` element styled as a grid. The grid layout is achieved +using CSS Grid, with three columns and three rows, each taking up an equal fraction of the available +space. The overall size of the grid is determined by the width and height properties, ensuring that +it is square-shaped and appropriately sized. + +Inside the grid, we place nine Square components, each with a value prop representing its position. +These Square components will eventually hold the game symbols (`'X'` or `'O'`) and handle user +interactions. + +Here's the code for the `Board` component: + +```tsx +export default function Board() { + return ( +
+ + + + + + + + + +
+ ) +} +``` + +This Board component sets up the basic structure for our game board by arranging nine squares in a +3x3 grid. It positions the squares neatly, providing a foundation for adding more features and +handling player interactions in the future. + +### Lifting state up + +Each `Square` component could maintain a part of the game's state. To check for a winner in a +tic-tac-toe game, the `Board` component would need to somehow know the state of each of the 9 +`Square` components. + +How would you approach that? At first, you might guess that the `Board` component needs to ask each +`Square` component for that `Square`'s component state. Although this approach is technically +possible in React, we discourage it because the code becomes difficult to understand, susceptible +to bugs, and hard to refactor. Instead, the best approach is to store the game's state in the +parent `Board` component instead of in each `Square` component. The `Board` component can tell each +`Square` component what to display by passing a prop, like you did when you passed a number to each +`Square` component. + +> [!IMPORTANT] +> To collect data from multiple children, or to have two or more child components +> communicate with each other, declare the shared state in their parent component instead. The +> parent component can pass that state back down to the children via props. This keeps the child +> components in sync with each other and with their parent. + +Let's take this opportunity to try it out. Edit the `Board` component so that it declares a state +variable named squares that defaults to an array of 9 nulls corresponding to the 9 squares: + +```tsx +import { create } from 'zustand' +import { combine } from 'zustand/middleware' + +const useGameStore = create( + combine({ squares: Array(9).fill(null) }, (set) => { + return { + setSquares: (nextSquares) => { + set((state) => ({ + squares: + typeof nextSquares === 'function' + ? nextSquares(state.squares) + : nextSquares, + })) + }, + } + }), +) + +export default function Board() { + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + + return ( +
+ {squares.map((square, squareIndex) => ( + + ))} +
+ ) +} +``` + +`Array(9).fill(null)` creates an array with nine elements and sets each of them to `null`. The +`useSquaresStore` declares a `squares` state that's initially set to that array. Each entry in the +array corresponds to the value of a square. When you fill the board in later, the squares array +will look like this: + +```ts +const squares = ['O', null, 'X', 'X', 'X', 'O', 'O', null, null] +``` + +Each Square will now receive a `value` prop that will either be `'X'`, `'O'`, or `null` for empty +squares. + +Next, you need to change what happens when a `Square` component is clicked. The `Board` component +now maintains which squares are filled. You'll need to create a way for the `Square` component to +update the `Board`'s component state. Since state is private to a component that defines it, you +cannot update the `Board`'s component state directly from `Square` component. + +Instead, you'll pass down a function from the Board component to the `Square` component, and you'll +have `Square` component call that function when a square is clicked. You'll start with the function +that the `Square` component will call when it is clicked. You'll call that function `onSquareClick`: + +Now you'll connect the `onSquareClick` prop to a function in the `Board` component that you'll name +`handleClick`. To connect `onSquareClick` to `handleClick` you'll pass an inline function to the +`onSquareClick` prop of the first Square component: + +```tsx + handleClick(i)} /> +``` + +Lastly, you will define the `handleClick` function inside the `Board` component to update the +squares array holding your board's state. + +The `handleClick` function should take the index of the square to update and create a copy of the +`squares` array (`nextSquares`). Then, `handleClick` updates the `nextSquares` array by adding `X` +to the square at the specified index (`i`) if is not already filled. + +```tsx{7-12,29} +export default function Board() { + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + + function handleClick(i) { + if (squares[i]) return + const nextSquares = squares.slice() + nextSquares[i] = 'X' + setSquares(nextSquares) + } + + return ( +
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ ) +} +``` + +> [!IMPORTANT] +> Note how in `handleClick` function, you call `.slice()` to create a copy of the squares array +> instead of modifying the existing array. + +### Taking turns + +It's now time to fix a major defect in this tic-tac-toe game: the `'O'`s cannot be used on the +board. + +You'll set the first move to be `'X'` by default. Let's keep track of this by adding another piece +of state to the `useGameStore` hook: + +```tsx{2,12-18} +const useGameStore = create( + combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => { + return { + setSquares: (nextSquares) => { + set((state) => ({ + squares: + typeof nextSquares === 'function' + ? nextSquares(state.squares) + : nextSquares, + })); + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })); + }, + }; + }) +); +``` + +Each time a player moves, `xIsNext` (a boolean) will be flipped to determine which player goes next +and the game's state will be saved. You'll update the Board's `handleClick` function to flip the +value of `xIsNext`: + +```tsx{2-5,10,15} +export default function Board() { + const [xIsNext, setXIsNext] = useGameStore((state) => [ + state.xIsNext, + state.setXIsNext, + ]) + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + const player = xIsNext ? 'X' : 'O' + + function handleClick(i) { + if (squares[i]) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + setXIsNext(!xIsNext) + } + + return ( +
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ ) +} +``` + +### Declaring a winner or draw + +Now that the players can take turns, you'll want to show when the game is won or drawn and there +are no more turns to make. To do this you'll add three helper functions. The first helper function +called `calculateWinner` that takes an array of 9 squares, checks for a winner and returns `'X'`, +`'O'`, or `null` as appropriate. The second helper function called `calculateTurns` that takes the +same array, checks for remaining turns by filtering out only `null` items, and returns the count of +them. The last helper called `calculateStatus` that takes the remaining turns, the winner, and the +current player (`'X' or 'O'`): + +```ts +function calculateWinner(squares) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i] + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a] + } + } + + return null +} + +function calculateTurns(squares) { + return squares.filter((square) => !square).length +} + +function calculateStatus(winner, turns, player) { + if (!winner && !turns) return 'Draw' + if (winner) return `Winner ${winner}` + return `Next player: ${player}` +} +``` + +You will use the result of `calculateWinner(squares)` in the Board component's `handleClick` +function to check if a player has won. You can perform this check at the same time you check if a +user has clicked a square that already has a `'X'` or and `'O'`. We'd like to return early in +both cases: + +```ts{2} +function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player' + setSquares(nextSquares) + setXIsNext(!xIsNext) +} +``` + +To let the players know when the game is over, you can display text such as `'Winner: X'` or +`'Winner: O'`. To do that you'll add a `status` section to the `Board` component. The status will +display the winner or draw if the game is over and if the game is ongoing you'll display which +player's turn is next: + +```tsx{10-11,13,25} +export default function Board() { + const [xIsNext, setXIsNext] = useGameStore((state) => [ + state.xIsNext, + state.setXIsNext, + ]) + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + setXIsNext(!xIsNext) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} +``` + +Congratulations! You now have a working tic-tac-toe game. And you've just learned the basics of +React and Zustand too. So you are the real winner here. Here is what the code should look like: + +```tsx +import { create } from 'zustand' +import { combine } from 'zustand/middleware' + +const useGameStore = create( + combine({ squares: Array(9).fill(null), xIsNext: true }, (set) => { + return { + setSquares: (nextSquares) => { + set((state) => ({ + squares: + typeof nextSquares === 'function' + ? nextSquares(state.squares) + : nextSquares, + })) + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })) + }, + } + }), +) + +function Square({ value, onSquareClick }) { + return ( + + ) +} + +export default function Board() { + const [xIsNext, setXIsNext] = useGameStore((state) => [ + state.xIsNext, + state.setXIsNext, + ]) + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + setXIsNext(!xIsNext) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} + +function calculateWinner(squares) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i] + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a] + } + } + + return null +} + +function calculateTurns(squares) { + return squares.filter((square) => !square).length +} + +function calculateStatus(winner, turns, player) { + if (!winner && !turns) return 'Draw' + if (winner) return `Winner ${winner}` + return `Next player: ${player}` +} +``` + +### Adding time travel + +As a final exercise, let's make it possible to “go back in time” and revisit previous moves in the +game. + +If you had directly modified the squares array, implementing this time-travel feature would be very +difficult. However, since you used `slice()` to create a new copy of the squares array after every +move, treating it as immutable, you can store every past version of the squares array and navigate +between them. + +You'll keep track of these past squares arrays in a new state variable called `history`. This +`history` array will store all board states, from the first move to the latest one, and will look +something like this: + +```ts +const history = [ + // First move + [null, null, null, null, null, null, null, null, null], + // Second move + ['X', null, null, null, null, null, null, null, null], + // Third move + ['X', 'O', null, null, null, null, null, null, null], + // and so on... +] +``` + +This approach allows you to easily navigate between different game states and implement the +time-travel feature. + +### Lifting state up, again + +Next, you will create a new top-level component called `Game` to display a list of past moves. This +is where you will store the `history` state that contains the entire game history. + +By placing the `history` state in the `Game` component, you can remove the `squares` state from the +`Board` component. You will now lift the state up from the `Board` component to the top-level `Game` +component. This change allows the `Game` component to have full control over the `Board`'s +component data and instruct the `Board` component to render previous turns from the `history`. + +First, add a `Game` component with `export default` and remove it from `Board` component. Here is +what the code should look like: + +```tsx{1,48-65} +function Board() { + const [xIsNext, setXIsNext] = useGameStore((state) => [ + state.xIsNext, + state.setXIsNext, + ]) + const [squares, setSquares] = useGameStore((state) => [ + state.squares, + state.setSquares, + ]) + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + setXIsNext(!xIsNext) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} + +export default function Game() { + return ( +
+
+ +
+
+
    {/*TODO*/}
+
+
+ ) +} +``` + +Add some state to the `useGameStore` hook to track the history of moves: + +```ts{2,4-11} +const useGameStore = create( + combine({ history: [Array(9).fill(null)], xIsNext: true }, (set) => { + return { + setHistory: (nextHistory) => { + set((state) => ({ + history: + typeof nextHistory === 'function' + ? nextHistory(state.history) + : nextHistory, + })) + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })) + }, + } + }), +) +``` + +Notice how `[Array(9).fill(null)]` creates an array with a single item, which is itself an array of +9 null values. + +To render the squares for the current move, you'll need to read the most recent squares array from +the `history` state. You don't need an extra state for this because you already have enough +information to calculate it during rendering: + +```tsx{2-3} +export default function Game() { + const { history, setHistory, xIsNext, setXIsNext } = useGameStore() + const currentSquares = history[history.length - 1] + + return ( +
+
+ +
+
+
    {/*TODO*/}
+
+
+ ) +} +``` + +Next, create a `handlePlay` function inside the `Game` component that will be called by the `Board` +component to update the game. Pass `xIsNext`, `currentSquares` and `handlePlay` as props to the +`Board` component: + +```tsx{5-7,18} +export default function Game() { + const { history, setHistory, xIsNext, setXIsNext } = useGameStore() + const currentSquares = history[history.length - 1] + + function handlePlay(nextSquares) { + // TODO + } + + return ( +
+
+ +
+
+
    {/*TODO*/}
+
+
+ ) +} +``` + +Let's make the `Board` component fully controlled by the props it receives. To do this, we'll modify +the `Board` component to accept three props: `xIsNext`, `squares`, and a new `onPlay` function that +the `Board` component can call with the updated squares array when a player makes a move. + +```tsx{1} +function Board({ xIsNext, squares, onPlay }) { + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + setSquares(nextSquares) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} +``` + +The `Board` component is now fully controlled by the props passed to it by the `Game` component. To +get the game working again, you need to implement the `handlePlay` function in the `Game` +component. + +What should `handlePlay` do when called? Previously, the `Board` component called `setSquares` with +an updated array; now it passes the updated squares array to `onPlay`. + +The `handlePlay` function needs to update the `Game` component's state to trigger a re-render. +Instead of using `setSquares`, you'll update the `history` state variable by appending the updated +squares array as a new `history` entry. You also need to toggle `xIsNext`, just as the `Board` +component used +to do. + +```ts{2-3} +function handlePlay(nextSquares) { + setHistory(history.concat([nextSquares])) + setXIsNext(!xIsNext) +} +``` + +At this point, you've moved the state to live in the `Game` component, and the UI should be fully +working, just as it was before the refactor. Here is what the code should look like at this point: + +```tsx +import { create } from 'zustand' +import { combine } from 'zustand/middleware' + +const useGameStore = create( + combine({ history: [Array(9).fill(null)], xIsNext: true }, (set) => { + return { + setHistory: (nextHistory) => { + set((state) => ({ + history: + typeof nextHistory === 'function' + ? nextHistory(state.history) + : nextHistory, + })) + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })) + }, + } + }), +) + +function Square({ value, onSquareClick }) { + return ( + + ) +} + +function Board({ xIsNext, squares, onPlay }) { + const winner = calculateWinner(squares) + const turns = calculateTurns(squares) + const player = xIsNext ? 'X' : 'O' + const status = calculateStatus(winner, turns, player) + + function handleClick(i) { + if (squares[i] || winner) return + const nextSquares = squares.slice() + nextSquares[i] = player + onPlay(nextSquares) + } + + return ( + <> +
{status}
+
+ {squares.map((square, squareIndex) => ( + handleClick(squareIndex)} + /> + ))} +
+ + ) +} + +export default function Game() { + const { history, setHistory, xIsNext, setXIsNext } = useGameStore() + const currentSquares = history[history.length - 1] + + function handlePlay(nextSquares) { + setHistory(history.concat([nextSquares])) + setXIsNext(!xIsNext) + } + + return ( +
+
+ +
+
+
    {/*TODO*/}
+
+
+ ) +} + +function calculateWinner(squares) { + const lines = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6], + ] + + for (let i = 0; i < lines.length; i++) { + const [a, b, c] = lines[i] + if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { + return squares[a] + } + } + + return null +} + +function calculateTurns(squares) { + return squares.filter((square) => !square).length +} + +function calculateStatus(winner, turns, player) { + if (!winner && !turns) return 'Draw' + if (winner) return `Winner ${winner}` + return `Next player: ${player}` +} +``` + +### Showing the past moves + +Since you are recording the tic-tac-toe game's history, you can now display a list of past moves to +the player. + +You already have an array of `history` moves in store, so now you need to transform it to an array +of React elements. In JavaScript, to transform one array into another, you can use the Array +`.map()` method: + +You'll use `map` to transform your `history` of moves into React elements representing buttons on the +screen, and display a list of buttons to **jump** to past moves. Let's `map` over the `history` in +the `Game` component: + +```tsx{26-41} +export default function Game() { + const { history, setHistory, xIsNext, setXIsNext } = useGameStore() + const currentSquares = history[history.length - 1] + + function handlePlay(nextSquares) { + setHistory(history.concat([nextSquares])) + setXIsNext(!xIsNext) + } + + function jumpTo(nextMove) { + // TODO + } + + return ( +
+
+ +
+
+
    + {history.map((_, historyIndex) => { + const description = + historyIndex > 0 + ? `Go to move #${historyIndex}` + : 'Go to game start' + + return ( +
  1. + +
  2. + ) + })} +
+
+
+ ) +} +``` + +Before you can implement the `jumpTo` function, you need the `Game` component to keep track of which +step the user is currently viewing. To do this, define a new state variable called `currentMove`, +which will start at `0`: + +```ts{3,14-21} +const useGameStore = create( + combine( + { history: [Array(9).fill(null)], currentMove: 0, xIsNext: true }, + (set) => { + return { + setHistory: (nextHistory) => { + set((state) => ({ + history: + typeof nextHistory === 'function' + ? nextHistory(state.history) + : nextHistory, + })) + }, + setCurrentMove: (nextCurrentMove) => { + set((state) => ({ + currentMove: + typeof nextCurrentMove === 'function' + ? nextCurrentMove(state.currentMove) + : nextCurrentMove, + })) + }, + setXIsNext: (nextXIsNext) => { + set((state) => ({ + xIsNext: + typeof nextXIsNext === 'function' + ? nextXIsNext(state.xIsNext) + : nextXIsNext, + })) + }, + } + }, + ), +) +``` + +Next, update the `jumpTo` function inside `Game` component to update that `currentMove`. You’ll +also set `xIsNext` to `true` if the number that you’re changing `currentMove` to is even. + +```ts{2-3} +function jumpTo(nextMove) { + setCurrentMove(nextMove) + setXIsNext(currentMove % 2 === 0) +} +``` + +You will now make two changes to the `handlePlay` function in the `Game` component, which is called +when you click on a square. + +- If you "go back in time" and then make a new move from that point, you only want to keep the + history up to that point. Instead of adding `nextSquares` after all items in the history (using + the Array `.concat()` method), you'll add it after all items in + `history.slice(0, currentMove + 1)` to keep only that portion of the old history. +- Each time a move is made, you need to update `currentMove` to point to the latest history entry. + +```ts{2-4} +function handlePlay(nextSquares) { + const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) + setHistory(nextHistory) + setCurrentMove(nextHistory.length - 1) + setXIsNext(!xIsNext) +} +``` + +Finally, you will modify the `Game` component to render the currently selected move, instead of +always rendering the final move: + +```tsx{2-10} +export default function Game() { + const { + history, + setHistory, + currentMove, + setCurrentMove, + xIsNext, + setXIsNext, + } = useGameStore() + const currentSquares = history[currentMove] + + function handlePlay(nextSquares) { + const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) + setHistory(nextHistory) + setCurrentMove(nextHistory.length - 1) + setXIsNext(!xIsNext) + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove) + setXIsNext(currentMove % 2 === 0) + } + + return ( +
+
+ +
+
+
    + {history.map((_, historyIndex) => { + const description = + historyIndex > 0 + ? `Go to move #${historyIndex}` + : 'Go to game start' + + return ( +
  1. + +
  2. + ) + })} +
+
+
+ ) +} +``` + +### Final cleanup + +If you look closely at the code, you'll see that `xIsNext` is `true` when `currentMove` is even and +`false` when `currentMove` is odd. This means that if you know the value of `currentMove`, you can +always determine what `xIsNext` should be. + +There's no need to store `xIsNext` separately in the state. It’s better to avoid redundant state +because it can reduce bugs and make your code easier to understand. Instead, you can calculate +`xIsNext` based on `currentMove`: + +```tsx{2,10,14} +export default function Game() { + const { history, setHistory, currentMove, setCurrentMove } = useGameStore() + const xIsNext = currentMove % 2 === 0 + const currentSquares = history[currentMove] + + function handlePlay(nextSquares) { + const nextHistory = history.slice(0, currentMove + 1).concat([nextSquares]) + setHistory(nextHistory) + setCurrentMove(nextHistory.length - 1) + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove) + } + + return ( +
+
+ +
+
+
    + {history.map((_, historyIndex) => { + const description = + historyIndex > 0 + ? `Go to move #${historyIndex}` + : 'Go to game start' + + return ( +
  1. + +
  2. + ) + })} +
+
+
+ ) +} +``` + +You no longer need the `xIsNext` state declaration or the calls to `setXIsNext`. Now, there’s no +chance for `xIsNext` to get out of sync with `currentMove`, even if you make a mistake while coding +the components. + +### Wrapping up + +Congratulations! You’ve created a tic-tac-toe game that: + +- Lets you play tic-tac-toe, +- Indicates when a player has won the game or when is drawn, +- Stores a game’s history as a game progresses, +- Allows players to review a game’s history and see previous versions of a game’s board. + +Nice work! We hope you now feel like you have a decent grasp of how React and Zustand works.