From 56cbee3252502a0aa0a83b4b7c15ad62565ed1a4 Mon Sep 17 00:00:00 2001 From: QuyetNh0905 Date: Mon, 3 Nov 2025 16:53:34 +0700 Subject: [PATCH 1/2] In src\content\learn\tutorial-tic-tac-toe.md, translate line 1-353 to vietnamese. --- src/content/learn/tutorial-tic-tac-toe.md | 5798 ++++++++++----------- 1 file changed, 2899 insertions(+), 2899 deletions(-) diff --git a/src/content/learn/tutorial-tic-tac-toe.md b/src/content/learn/tutorial-tic-tac-toe.md index c80c7f5fe..ee5b14d60 100644 --- a/src/content/learn/tutorial-tic-tac-toe.md +++ b/src/content/learn/tutorial-tic-tac-toe.md @@ -1,2918 +1,2918 @@ ---- -title: 'Tutorial: Tic-Tac-Toe' ---- - - - -You will build a small tic-tac-toe game during this tutorial. This tutorial does not assume any 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. - - - - - -This tutorial is designed for people who prefer to **learn by doing** and want to quickly try making something tangible. If you prefer learning each concept step by step, start with [Describing the UI.](/learn/describing-the-ui) - - - -The tutorial is divided into several sections: - -- [Setup for the tutorial](#setup-for-the-tutorial) will give you **a starting point** to follow the tutorial. -- [Overview](#overview) will teach you **the fundamentals** of React: components, props, and state. -- [Completing the game](#completing-the-game) will teach you **the most common techniques** in React development. -- [Adding time travel](#adding-time-travel) will give you **a deeper insight** into the unique strengths of React. - -### What are you building? {/*what-are-you-building*/} - -In this tutorial, you'll build an interactive tic-tac-toe game with React. - -You can see what it will look like when you're finished here: - - - -```js src/App.js -import { useState } from 'react'; - -function Square({ value, onSquareClick }) { - return ( - - ); -} - -function Board({ xIsNext, squares, onPlay }) { - function handleClick(i) { - if (calculateWinner(squares) || squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = 'X'; - } else { - nextSquares[i] = 'O'; - } - onPlay(nextSquares); - } - - const winner = calculateWinner(squares); - let status; - if (winner) { - status = 'Winner: ' + winner; - } else { - status = 'Next player: ' + (xIsNext ? 'X' : 'O'); - } - - return ( - <> -
{status}
-
- handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
-
- handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
-
- handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
- - ); -} - -export default function Game() { - const [history, setHistory] = useState([Array(9).fill(null)]); - const [currentMove, setCurrentMove] = useState(0); - 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); - } - - const moves = history.map((squares, move) => { - let description; - if (move > 0) { - description = 'Go to move #' + move; - } else { - description = 'Go to game start'; - } - return ( -
  • - -
  • - ); - }); - - return ( -
    -
    - -
    -
    -
      {moves}
    -
    -
    - ); -} - -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; -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - -If the code doesn't make sense to you yet, or if you are unfamiliar with the code's syntax, don't worry! The goal of this tutorial is to help you understand React and its syntax. - -We recommend that you check out the tic-tac-toe game above before continuing with the tutorial. One of the features that you'll notice is that there is a numbered list to the right of the game's board. This list gives you a history of all of the moves that have occurred in the game, and it is updated as the game progresses. - -Once you've played around with the finished tic-tac-toe game, keep scrolling. You'll start with a simpler template in this tutorial. Our next step is to set you up so that you can start building the game. - -## Setup for the tutorial {/*setup-for-the-tutorial*/} - -In the live code editor below, click **Fork** in the top-right corner to open the editor in a new tab using the website CodeSandbox. CodeSandbox lets you write code in your browser and preview how your users will see the app you've created. The new tab should display an empty square and the starter code for this tutorial. - - - -```js src/App.js -export default function Square() { - return ; -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - - - - - -You can also follow this tutorial using your local development environment. To do this, you need to: - -1. Install [Node.js](https://nodejs.org/en/) -1. In the CodeSandbox tab you opened earlier, press the top-left corner button to open the menu, and then choose **Download Sandbox** in that menu to download an archive of the files locally -1. Unzip the archive, then open a terminal and `cd` to the directory you unzipped -1. Install the dependencies with `npm install` -1. Run `npm start` to start a local server and follow the prompts to view the code running in a browser - -If you get stuck, don't let this stop you! Follow along online instead and try a local setup again later. - - - -## Overview {/*overview*/} - -Now that you're set up, let's get an overview of React! - -### Inspecting the starter code {/*inspecting-the-starter-code*/} - -In CodeSandbox you'll see three main sections: - -![CodeSandbox with starter code](../images/tutorial/react-starter-code-codesandbox.png) - -1. The _Files_ section with a list of files like `App.js`, `index.js`, `styles.css` and a folder called `public` -1. The _code editor_ where you'll see the source code of your selected file -1. The _browser_ section where you'll see how the code you've written will be displayed - -The `App.js` file should be selected in the _Files_ section. The contents of that file in the _code editor_ should be: - -```jsx -export default function Square() { - return ; -} -``` - -The _browser_ section should be displaying a square with an X in it like this: - -![x-filled square](../images/tutorial/x-filled-square.png) - -Now let's have a look at the files in the starter code. - +--- +title: 'Hướng dẫn: Tic-Tac-Toe' +--- + + + +Trong hướng dẫn này, bạn sẽ xây dựng một trò chơi tic-tac-toe nhỏ. Hướng dẫn này không yêu cầu kiến thức React sẵn có. Các kỹ thuật bạn sẽ học trong hướng dẫn là nền tảng để xây dựng bất kỳ ứng dụng React nào, và việc hiểu đầy đủ nó sẽ giúp bạn có hiểu biết sâu sắc về React. + + + + + +Hướng dẫn này được thiết kế cho những người thích **học qua thực hành** và muốn nhanh chóng thử làm một cái gì đó cụ thể. Nếu bạn thích học từng khái niệm từng bước một, hãy bắt đầu với [Mô tả UI.](/learn/describing-the-ui) + + + +Hướng dẫn được chia thành nhiều phần: + +- [Thiết lập cho hướng dẫn](#setup-for-the-tutorial) sẽ cung cấp cho bạn **điểm bắt đầu** để theo dõi hướng dẫn. +- [Tổng quan](#overview) sẽ dạy bạn **những điều cơ bản** của React: components, props, và state. +- [Hoàn thiện trò chơi](#completing-the-game) sẽ dạy bạn **các kỹ thuật phổ biến nhất** trong phát triển React. +- [Thêm tính năng du hành thời gian](#adding-time-travel) sẽ cho bạn **cái nhìn sâu sắc hơn** về những điểm mạnh độc đáo của React. + +### Bạn sẽ xây dựng gì? {/*what-are-you-building*/} + +Trong hướng dẫn này, bạn sẽ xây dựng một trò chơi tic-tac-toe tương tác với React. + +Bạn có thể xem trò chơi sẽ trông như thế nào khi hoàn thành ở đây: + + + +```js src/App.js +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +function Board({ xIsNext, squares, onPlay }) { + function handleClick(i) { + if (calculateWinner(squares) || squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = 'X'; + } else { + nextSquares[i] = 'O'; + } + onPlay(nextSquares); + } + + const winner = calculateWinner(squares); + let status; + if (winner) { + status = 'Winner: ' + winner; + } else { + status = 'Next player: ' + (xIsNext ? 'X' : 'O'); + } + + return ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + 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); + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + +Nếu mã code chưa có ý nghĩa với bạn, hoặc nếu bạn chưa quen với cú pháp của code, đừng lo lắng! Mục tiêu của hướng dẫn này là giúp bạn hiểu React và cú pháp của nó. + +Chúng tôi khuyên bạn nên xem qua trò chơi tic-tac-toe ở trên trước khi tiếp tục với hướng dẫn. Một trong những tính năng mà bạn sẽ nhận thấy là có một danh sách đánh số ở bên phải bảng chơi. Danh sách này cung cấp lịch sử tất cả các nước đi đã xảy ra trong trò chơi, và nó được cập nhật khi trò chơi diễn ra. + +Sau khi bạn đã chơi thử với trò chơi tic-tac-toe đã hoàn thành, hãy tiếp tục cuộn xuống. Bạn sẽ bắt đầu với một template đơn giản hơn trong hướng dẫn này. Bước tiếp theo của chúng tôi là thiết lập để bạn có thể bắt đầu xây dựng trò chơi. + +## Thiết lập cho hướng dẫn {/*setup-for-the-tutorial*/} + +Trong trình soạn thảo code trực tiếp bên dưới, nhấp vào **Fork** ở góc trên bên phải để mở trình soạn thảo trong tab mới sử dụng trang web CodeSandbox. CodeSandbox cho phép bạn viết code trong trình duyệt và xem trước cách người dùng sẽ thấy ứng dụng mà bạn đã tạo. Tab mới sẽ hiển thị một ô vuông trống và mã code khởi đầu cho hướng dẫn này. + + + +```js src/App.js +export default function Square() { + return ; +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + + + + + +Bạn cũng có thể theo dõi hướng dẫn này bằng cách sử dụng môi trường phát triển local của mình. Để làm điều này, bạn cần: + +1. Cài đặt [Node.js](https://nodejs.org/en/) +1. Trong tab CodeSandbox mà bạn đã mở trước đó, nhấn nút ở góc trên bên trái để mở menu, sau đó chọn **Download Sandbox** trong menu đó để tải xuống một file nén của các file về máy local +1. Giải nén file đó, sau đó mở terminal và `cd` vào thư mục bạn đã giải nén +1. Cài đặt các dependencies bằng `npm install` +1. Chạy `npm start` để khởi động server local và làm theo các hướng dẫn để xem code chạy trong trình duyệt + +Nếu bạn gặp khó khăn, đừng để điều này làm bạn dừng lại! Thay vào đó, hãy tiếp tục theo dõi trực tuyến và thử thiết lập local lại sau. + + + +## Tổng quan {/*overview*/} + +Bây giờ bạn đã thiết lập xong, hãy tìm hiểu tổng quan về React! + +### Kiểm tra mã code khởi đầu {/*inspecting-the-starter-code*/} + +Trong CodeSandbox, bạn sẽ thấy ba phần chính: + +![CodeSandbox with starter code](../images/tutorial/react-starter-code-codesandbox.png) + +1. Phần _Files_ với danh sách các file như `App.js`, `index.js`, `styles.css` và một thư mục có tên `public` +1. Trình _soạn thảo code_ nơi bạn sẽ thấy mã nguồn của file bạn đã chọn +1. Phần _browser_ nơi bạn sẽ thấy cách code bạn đã viết sẽ được hiển thị + +File `App.js` nên được chọn trong phần _Files_. Nội dung của file đó trong trình _soạn thảo code_ sẽ là: + +```jsx +export default function Square() { + return ; +} +``` + +Phần _browser_ nên hiển thị một ô vuông có chữ X bên trong như sau: + +![x-filled square](../images/tutorial/x-filled-square.png) + +Bây giờ hãy xem các file trong mã code khởi đầu. + #### `App.js` {/*appjs*/} - -The code in `App.js` creates a _component_. In React, a component is a piece of reusable code that represents a part of a user interface. Components are used to render, manage, and update the UI elements in your application. Let's look at the component line by line to see what's going on: - -```js {1} -export default function Square() { - return ; -} -``` - -The first line defines a function called `Square`. The `export` JavaScript keyword makes this function accessible outside of this file. The `default` keyword tells other files using your code that it's the main function in your file. - -```js {2} -export default function Square() { - return ; -} -``` - -The second line returns a button. The `return` JavaScript keyword means whatever comes after is returned as a value to the caller of the function. `` closes the JSX element to indicate that any following content shouldn't be placed inside the button. - + +Mã code trong `App.js` tạo ra một _component_. Trong React, một component là một đoạn code có thể tái sử dụng đại diện cho một phần của giao diện người dùng. Components được sử dụng để render, quản lý và cập nhật các phần tử UI trong ứng dụng của bạn. Hãy xem component từng dòng một để hiểu điều gì đang xảy ra: + +```js {1} +export default function Square() { + return ; +} +``` + +Dòng đầu tiên định nghĩa một function có tên `Square`. Từ khóa JavaScript `export` làm cho function này có thể truy cập được từ bên ngoài file này. Từ khóa `default` cho các file khác sử dụng code của bạn biết rằng đây là function chính trong file của bạn. + +```js {2} +export default function Square() { + return ; +} +``` + +Dòng thứ hai trả về một button. Từ khóa JavaScript `return` có nghĩa là bất cứ thứ gì đi sau nó sẽ được trả về như một giá trị cho người gọi function. `` đóng phần tử JSX để chỉ ra rằng bất kỳ nội dung nào sau đó không nên được đặt bên trong button. + #### `styles.css` {/*stylescss*/} - -Click on the file labeled `styles.css` in the _Files_ section of CodeSandbox. This file defines the styles for your React app. The first two _CSS selectors_ (`*` and `body`) define the style of large parts of your app while the `.square` selector defines the style of any component where the `className` property is set to `square`. In your code, that would match the button from your Square component in the `App.js` file. - + +Nhấp vào file có nhãn `styles.css` trong phần _Files_ của CodeSandbox. File này định nghĩa các kiểu cho ứng dụng React của bạn. Hai _bộ chọn CSS_ đầu tiên (`*` và `body`) định nghĩa kiểu cho các phần lớn của ứng dụng của bạn trong khi bộ chọn `.square` định nghĩa kiểu cho bất kỳ component nào có thuộc tính `className` được đặt thành `square`. Trong code của bạn, điều đó sẽ khớp với button từ component Square của bạn trong file `App.js`. + #### `index.js` {/*indexjs*/} - -Click on the file labeled `index.js` in the _Files_ section of CodeSandbox. You won't be editing this file during the tutorial but it is the bridge between the component you created in the `App.js` file and the web browser. - -```jsx -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import './styles.css'; - -import App from './App'; -``` - -Lines 1-5 bring all the necessary pieces together: - -* React -* React's library to talk to web browsers (React DOM) -* the styles for your components -* the component you created in `App.js`. - -The remainder of the file brings all the pieces together and injects the final product into `index.html` in the `public` folder. - -### Building the board {/*building-the-board*/} - -Let's get back to `App.js`. This is where you'll spend the rest of the tutorial. - -Currently the board is only a single square, but you need nine! If you just try and copy paste your square to make two squares like this: - -```js {2} -export default function Square() { - return ; -} -``` - -You'll get this error: - - - -/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX Fragment `<>...`? - - - -React components need to return a single JSX element and not multiple adjacent JSX elements like two buttons. To fix this you can use *Fragments* (`<>` and ``) to wrap multiple adjacent JSX elements like this: - -```js {3-6} -export default function Square() { - return ( - <> - - - - ); -} -``` - -Now you should see: - -![two x-filled squares](../images/tutorial/two-x-filled-squares.png) - -Great! Now you just need to copy-paste a few times to add nine squares and... - -![nine x-filled squares in a line](../images/tutorial/nine-x-filled-squares.png) - -Oh no! The squares are all in a single line, not in a grid like you need for our board. To fix this you'll need to group your squares into rows with `div`s and add some CSS classes. While you're at it, you'll give each square a number to make sure you know where each square is displayed. - -In the `App.js` file, update the `Square` component to look like this: - -```js {3-19} -export default function Square() { - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -The CSS defined in `styles.css` styles the divs with the `className` of `board-row`. Now that you've grouped your components into rows with the styled `div`s you have your tic-tac-toe board: - -![tic-tac-toe board filled with numbers 1 through 9](../images/tutorial/number-filled-board.png) - -But you now have a problem. Your component named `Square`, really isn't a square anymore. Let's fix that by changing the name to `Board`: - -```js {1} -export default function Board() { - //... -} -``` - -At this point your code should look something like this: - - - -```js -export default function Board() { - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - - - -Psssst... That's a lot to type! It's okay to copy and paste code from this page. However, if you're up for a little challenge, we recommend only copying code that you've manually typed at least once yourself. - - - + +Nhấp vào file có nhãn `index.js` trong phần _Files_ của CodeSandbox. Bạn sẽ không chỉnh sửa file này trong suốt hướng dẫn nhưng nó là cầu nối giữa component bạn đã tạo trong file `App.js` và trình duyệt web. + +```jsx +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './styles.css'; + +import App from './App'; +``` + +Các dòng 1-5 tập hợp tất cả các phần cần thiết lại với nhau: + +* React +* Thư viện React để giao tiếp với trình duyệt web (React DOM) +* các kiểu cho components của bạn +* component bạn đã tạo trong `App.js`. + +Phần còn lại của file tập hợp tất cả các phần lại với nhau và chèn sản phẩm cuối cùng vào `index.html` trong thư mục `public`. + +### Xây dựng bảng chơi {/*building-the-board*/} + +Hãy quay lại `App.js`. Đây là nơi bạn sẽ dành phần còn lại của hướng dẫn. + +Hiện tại bảng chơi chỉ có một ô vuông, nhưng bạn cần chín ô! Nếu bạn chỉ thử và sao chép dán ô vuông của mình để tạo hai ô vuông như thế này: + +```js {2} +export default function Square() { + return ; +} +``` + +You'll get this error: + + + +/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX Fragment `<>...`? + + + +React components need to return a single JSX element and not multiple adjacent JSX elements like two buttons. To fix this you can use *Fragments* (`<>` and ``) to wrap multiple adjacent JSX elements like this: + +```js {3-6} +export default function Square() { + return ( + <> + + + + ); +} +``` + +Now you should see: + +![two x-filled squares](../images/tutorial/two-x-filled-squares.png) + +Great! Now you just need to copy-paste a few times to add nine squares and... + +![nine x-filled squares in a line](../images/tutorial/nine-x-filled-squares.png) + +Oh no! The squares are all in a single line, not in a grid like you need for our board. To fix this you'll need to group your squares into rows with `div`s and add some CSS classes. While you're at it, you'll give each square a number to make sure you know where each square is displayed. + +In the `App.js` file, update the `Square` component to look like this: + +```js {3-19} +export default function Square() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` + +The CSS defined in `styles.css` styles the divs with the `className` of `board-row`. Now that you've grouped your components into rows with the styled `div`s you have your tic-tac-toe board: + +![tic-tac-toe board filled with numbers 1 through 9](../images/tutorial/number-filled-board.png) + +But you now have a problem. Your component named `Square`, really isn't a square anymore. Let's fix that by changing the name to `Board`: + +```js {1} +export default function Board() { + //... +} +``` + +At this point your code should look something like this: + + + +```js +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + + + +Psssst... That's a lot to type! It's okay to copy and paste code from this page. However, if you're up for a little challenge, we recommend only copying code that you've manually typed at least once yourself. + + + ### Passing data through props {/*passing-data-through-props*/} - -Next, you'll want to change the value of a square from empty to "X" when the user clicks on the square. With how you've built the board so far you would need to copy-paste the code that updates the square nine times (once for each square you have)! Instead of copy-pasting, React's component architecture allows you to create a reusable component to avoid messy, duplicated code. - -First, you are going to copy the line defining your first square (``) from your `Board` component into a new `Square` component: - -```js {1-3} -function Square() { - return ; -} - -export default function Board() { - // ... -} -``` - -Then you'll update the Board component to render that `Square` component using JSX syntax: - -```js {5-19} -// ... -export default function Board() { - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -Note how unlike the browser `div`s, your own components `Board` and `Square` must start with a capital letter. - -Let's take a look: - -![one-filled board](../images/tutorial/board-filled-with-ones.png) - -Oh no! You lost the numbered squares you had before. Now each square says "1". To fix this, you will use *props* to pass the value each square should have from the parent component (`Board`) to its child (`Square`). - -Update the `Square` component to read the `value` prop that you'll pass from the `Board`: - -```js {1} -function Square({ value }) { - return ; -} -``` - -`function Square({ value })` indicates the Square component can be passed a prop called `value`. - -Now you want to display that `value` instead of `1` inside every square. Try doing it like this: - -```js {2} -function Square({ value }) { - return ; -} -``` - -Oops, this is not what you wanted: - -![value-filled board](../images/tutorial/board-filled-with-value.png) - -You wanted to render the JavaScript variable called `value` from your component, not the word "value". To "escape into JavaScript" from JSX, you need curly braces. Add curly braces around `value` in JSX like so: - -```js {2} -function Square({ value }) { - return ; -} -``` - -For now, you should see an empty board: - -![empty board](../images/tutorial/empty-board.png) - -This is because the `Board` component hasn't passed the `value` prop to each `Square` component it renders yet. To fix it you'll add the `value` prop to each `Square` component rendered by the `Board` component: - -```js {5-7,10-12,15-17} -export default function Board() { - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -Now you should see a grid of numbers again: - -![tic-tac-toe board filled with numbers 1 through 9](../images/tutorial/number-filled-board.png) - -Your updated code should look like this: - - - -```js src/App.js -function Square({ value }) { - return ; -} - -export default function Board() { - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - + +Next, you'll want to change the value of a square from empty to "X" when the user clicks on the square. With how you've built the board so far you would need to copy-paste the code that updates the square nine times (once for each square you have)! Instead of copy-pasting, React's component architecture allows you to create a reusable component to avoid messy, duplicated code. + +First, you are going to copy the line defining your first square (``) from your `Board` component into a new `Square` component: + +```js {1-3} +function Square() { + return ; +} + +export default function Board() { + // ... +} +``` + +Then you'll update the Board component to render that `Square` component using JSX syntax: + +```js {5-19} +// ... +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` + +Note how unlike the browser `div`s, your own components `Board` and `Square` must start with a capital letter. + +Let's take a look: + +![one-filled board](../images/tutorial/board-filled-with-ones.png) + +Oh no! You lost the numbered squares you had before. Now each square says "1". To fix this, you will use *props* to pass the value each square should have from the parent component (`Board`) to its child (`Square`). + +Update the `Square` component to read the `value` prop that you'll pass from the `Board`: + +```js {1} +function Square({ value }) { + return ; +} +``` + +`function Square({ value })` indicates the Square component can be passed a prop called `value`. + +Now you want to display that `value` instead of `1` inside every square. Try doing it like this: + +```js {2} +function Square({ value }) { + return ; +} +``` + +Oops, this is not what you wanted: + +![value-filled board](../images/tutorial/board-filled-with-value.png) + +You wanted to render the JavaScript variable called `value` from your component, not the word "value". To "escape into JavaScript" from JSX, you need curly braces. Add curly braces around `value` in JSX like so: + +```js {2} +function Square({ value }) { + return ; +} +``` + +For now, you should see an empty board: + +![empty board](../images/tutorial/empty-board.png) + +This is because the `Board` component hasn't passed the `value` prop to each `Square` component it renders yet. To fix it you'll add the `value` prop to each `Square` component rendered by the `Board` component: + +```js {5-7,10-12,15-17} +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` + +Now you should see a grid of numbers again: + +![tic-tac-toe board filled with numbers 1 through 9](../images/tutorial/number-filled-board.png) + +Your updated code should look like this: + + + +```js src/App.js +function Square({ value }) { + return ; +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + ### Making an interactive component {/*making-an-interactive-component*/} - -Let's fill the `Square` component with an `X` when you click it. Declare a function called `handleClick` inside of the `Square`. Then, add `onClick` to the props of the button JSX element returned from the `Square`: - -```js {2-4,9} -function Square({ value }) { - function handleClick() { - console.log('clicked!'); - } - - return ( - - ); -} -``` - -If you click on a square now, you should see a log saying `"clicked!"` in the _Console_ tab at the bottom of the _Browser_ section in CodeSandbox. Clicking the square more than once will log `"clicked!"` again. Repeated console logs with the same message will not create more lines in the console. Instead, you will see an incrementing counter next to your first `"clicked!"` log. - - - -If you are following this tutorial using your local development environment, you need to open your browser's Console. For example, if you use the Chrome browser, you can view the Console with the keyboard shortcut **Shift + Ctrl + J** (on Windows/Linux) or **Option + ⌘ + J** (on macOS). - - - -As a next step, you want the Square component to "remember" that it got clicked, and fill it with an "X" mark. To "remember" things, components use *state*. - -React provides a special function called `useState` that you can call from your component to let it "remember" things. Let's store the current value of the `Square` in state, and change it when the `Square` is clicked. - -Import `useState` at the top of the file. Remove the `value` prop from the `Square` component. Instead, add a new line at the start of the `Square` that calls `useState`. Have it return a state variable called `value`: - -```js {1,3,4} -import { useState } from 'react'; - -function Square() { - const [value, setValue] = useState(null); - - function handleClick() { - //... -``` - -`value` stores the value and `setValue` is a function that can be used to change the value. The `null` passed to `useState` is used as the initial value for this state variable, so `value` here starts off equal to `null`. - -Since the `Square` component no longer accepts props anymore, you'll remove the `value` prop from all nine of the Square components created by the Board component: - -```js {6-8,11-13,16-18} -// ... -export default function Board() { - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -Now you'll change `Square` to display an "X" when clicked. Replace the `console.log("clicked!");` event handler with `setValue('X');`. Now your `Square` component looks like this: - -```js {5} -function Square() { - const [value, setValue] = useState(null); - - function handleClick() { - setValue('X'); - } - - return ( - - ); -} -``` - -By calling this `set` function from an `onClick` handler, you're telling React to re-render that `Square` whenever its ` - ); -} - -export default function Board() { - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - - - + +Let's fill the `Square` component with an `X` when you click it. Declare a function called `handleClick` inside of the `Square`. Then, add `onClick` to the props of the button JSX element returned from the `Square`: + +```js {2-4,9} +function Square({ value }) { + function handleClick() { + console.log('clicked!'); + } + + return ( + + ); +} +``` + +If you click on a square now, you should see a log saying `"clicked!"` in the _Console_ tab at the bottom of the _Browser_ section in CodeSandbox. Clicking the square more than once will log `"clicked!"` again. Repeated console logs with the same message will not create more lines in the console. Instead, you will see an incrementing counter next to your first `"clicked!"` log. + + + +If you are following this tutorial using your local development environment, you need to open your browser's Console. For example, if you use the Chrome browser, you can view the Console with the keyboard shortcut **Shift + Ctrl + J** (on Windows/Linux) or **Option + ⌘ + J** (on macOS). + + + +As a next step, you want the Square component to "remember" that it got clicked, and fill it with an "X" mark. To "remember" things, components use *state*. + +React provides a special function called `useState` that you can call from your component to let it "remember" things. Let's store the current value of the `Square` in state, and change it when the `Square` is clicked. + +Import `useState` at the top of the file. Remove the `value` prop from the `Square` component. Instead, add a new line at the start of the `Square` that calls `useState`. Have it return a state variable called `value`: + +```js {1,3,4} +import { useState } from 'react'; + +function Square() { + const [value, setValue] = useState(null); + + function handleClick() { + //... +``` + +`value` stores the value and `setValue` is a function that can be used to change the value. The `null` passed to `useState` is used as the initial value for this state variable, so `value` here starts off equal to `null`. + +Since the `Square` component no longer accepts props anymore, you'll remove the `value` prop from all nine of the Square components created by the Board component: + +```js {6-8,11-13,16-18} +// ... +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` + +Now you'll change `Square` to display an "X" when clicked. Replace the `console.log("clicked!");` event handler with `setValue('X');`. Now your `Square` component looks like this: + +```js {5} +function Square() { + const [value, setValue] = useState(null); + + function handleClick() { + setValue('X'); + } + + return ( + + ); +} +``` + +By calling this `set` function from an `onClick` handler, you're telling React to re-render that `Square` whenever its ` + ); +} + +export default function Board() { + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + + + ### React Developer Tools {/*react-developer-tools*/} - -React DevTools let you check the props and the state of your React components. You can find the React DevTools tab at the bottom of the _browser_ section in CodeSandbox: - -![React DevTools in CodeSandbox](../images/tutorial/codesandbox-devtools.png) - -To inspect a particular component on the screen, use the button in the top left corner of React DevTools: - -![Selecting components on the page with React DevTools](../images/tutorial/devtools-select.gif) - - - -For local development, React DevTools is available as a [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/), and [Edge](https://microsoftedge.microsoft.com/addons/detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil) browser extension. Install it, and the *Components* tab will appear in your browser Developer Tools for sites using React. - - - + +React DevTools let you check the props and the state of your React components. You can find the React DevTools tab at the bottom of the _browser_ section in CodeSandbox: + +![React DevTools in CodeSandbox](../images/tutorial/codesandbox-devtools.png) + +To inspect a particular component on the screen, use the button in the top left corner of React DevTools: + +![Selecting components on the page with React DevTools](../images/tutorial/devtools-select.gif) + + + +For local development, React DevTools is available as a [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/), and [Edge](https://microsoftedge.microsoft.com/addons/detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil) browser extension. Install it, and the *Components* tab will appear in your browser Developer Tools for sites using React. + + + ## Completing the game {/*completing-the-game*/} - -By this point, you have all the basic building blocks for your tic-tac-toe game. To have a complete game, you now need to alternate placing "X"s and "O"s on the board, and you need a way to determine a winner. - + +By this point, you have all the basic building blocks for your tic-tac-toe game. To have a complete game, you now need to alternate placing "X"s and "O"s on the board, and you need a way to determine a winner. + ### Lifting state up {/*lifting-state-up*/} - -Currently, each `Square` component maintains a part of the game's state. To check for a winner in a tic-tac-toe game, the `Board` 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` needs to "ask" each `Square` for that `Square`'s 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`. The `Board` component can tell each `Square` what to display by passing a prop, like you did when you passed a number to each Square. - -**To collect data from multiple children, or to have two 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.** - -Lifting state into a parent component is common when React components are refactored. - -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: - -```js {3} -// ... -export default function Board() { - const [squares, setSquares] = useState(Array(9).fill(null)); - return ( - // ... - ); -} -``` - -`Array(9).fill(null)` creates an array with nine elements and sets each of them to `null`. The `useState()` call around it declares a `squares` state variable 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: - -```jsx -['O', null, 'X', 'X', 'X', 'O', 'O', null, null] -``` - -Now your `Board` component needs to pass the `value` prop down to each `Square` that it renders: - -```js {6-8,11-13,16-18} -export default function Board() { - const [squares, setSquares] = useState(Array(9).fill(null)); - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -Next, you'll edit the `Square` component to receive the `value` prop from the Board component. This will require removing the Square component's own stateful tracking of `value` and the button's `onClick` prop: - -```js {1,2} -function Square({value}) { - return ; -} -``` - -At this point you should see an empty tic-tac-toe board: - -![empty board](../images/tutorial/empty-board.png) - -And your code should look like this: - - - -```js src/App.js -import { useState } from 'react'; - -function Square({ value }) { - return ; -} - -export default function Board() { - const [squares, setSquares] = useState(Array(9).fill(null)); - return ( - <> -
    - - - -
    -
    - - - -
    -
    - - - -
    - - ); -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - -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` is clicked. The `Board` component now maintains which squares are filled. You'll need to create a way for the `Square` to update the `Board`'s state. Since state is private to a component that defines it, you cannot update the `Board`'s state directly from `Square`. - -Instead, you'll pass down a function from the `Board` component to the `Square` component, and you'll have `Square` 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`: - -```js {3} -function Square({ value }) { - return ( - - ); -} -``` - -Next, you'll add the `onSquareClick` function to the `Square` component's props: - -```js {1} -function Square({ value, onSquareClick }) { - return ( - - ); -} -``` - -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 a function to the `onSquareClick` prop of the first `Square` component: - -```js {7} -export default function Board() { - const [squares, setSquares] = useState(Array(9).fill(null)); - - return ( - <> -
    - - //... - ); -} -``` - -Lastly, you will define the `handleClick` function inside the Board component to update the `squares` array holding your board's state: - -```js {4-8} -export default function Board() { - const [squares, setSquares] = useState(Array(9).fill(null)); - - function handleClick() { - const nextSquares = squares.slice(); - nextSquares[0] = "X"; - setSquares(nextSquares); - } - - return ( - // ... - ) -} -``` - -The `handleClick` function creates a copy of the `squares` array (`nextSquares`) with the JavaScript `slice()` Array method. Then, `handleClick` updates the `nextSquares` array to add `X` to the first (`[0]` index) square. - -Calling the `setSquares` function lets React know the state of the component has changed. This will trigger a re-render of the components that use the `squares` state (`Board`) as well as its child components (the `Square` components that make up the board). - - - -JavaScript supports [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) which means an inner function (e.g. `handleClick`) has access to variables and functions defined in an outer function (e.g. `Board`). The `handleClick` function can read the `squares` state and call the `setSquares` method because they are both defined inside of the `Board` function. - - - -Now you can add X's to the board... but only to the upper left square. Your `handleClick` function is hardcoded to update the index for the upper left square (`0`). Let's update `handleClick` to be able to update any square. Add an argument `i` to the `handleClick` function that takes the index of the square to update: - -```js {4,6} -export default function Board() { - const [squares, setSquares] = useState(Array(9).fill(null)); - - function handleClick(i) { - const nextSquares = squares.slice(); - nextSquares[i] = "X"; - setSquares(nextSquares); - } - - return ( - // ... - ) -} -``` - -Next, you will need to pass that `i` to `handleClick`. You could try to set the `onSquareClick` prop of square to be `handleClick(0)` directly in the JSX like this, but it won't work: - -```jsx - -``` - -Here is why this doesn't work. The `handleClick(0)` call will be a part of rendering the board component. Because `handleClick(0)` alters the state of the board component by calling `setSquares`, your entire board component will be re-rendered again. But this runs `handleClick(0)` again, leading to an infinite loop: - - - -Too many re-renders. React limits the number of renders to prevent an infinite loop. - - - -Why didn't this problem happen earlier? - -When you were passing `onSquareClick={handleClick}`, you were passing the `handleClick` function down as a prop. You were not calling it! But now you are *calling* that function right away--notice the parentheses in `handleClick(0)`--and that's why it runs too early. You don't *want* to call `handleClick` until the user clicks! - -You could fix this by creating a function like `handleFirstSquareClick` that calls `handleClick(0)`, a function like `handleSecondSquareClick` that calls `handleClick(1)`, and so on. You would pass (rather than call) these functions down as props like `onSquareClick={handleFirstSquareClick}`. This would solve the infinite loop. - -However, defining nine different functions and giving each of them a name is too verbose. Instead, let's do this: - -```js {6} -export default function Board() { - // ... - return ( - <> -
    - handleClick(0)} /> - // ... - ); -} -``` - -Notice the new `() =>` syntax. Here, `() => handleClick(0)` is an *arrow function,* which is a shorter way to define functions. When the square is clicked, the code after the `=>` "arrow" will run, calling `handleClick(0)`. - -Now you need to update the other eight squares to call `handleClick` from the arrow functions you pass. Make sure that the argument for each call of the `handleClick` corresponds to the index of the correct square: - -```js {6-8,11-13,16-18} -export default function Board() { - // ... - return ( - <> -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -}; -``` - -Now you can again add X's to any square on the board by clicking on them: - -![filling the board with X](../images/tutorial/tictac-adding-x-s.gif) - -But this time all the state management is handled by the `Board` component! - -This is what your code should look like: - - - -```js src/App.js -import { useState } from 'react'; - -function Square({ value, onSquareClick }) { - return ( - - ); -} - -export default function Board() { - const [squares, setSquares] = useState(Array(9).fill(null)); - - function handleClick(i) { - const nextSquares = squares.slice(); - nextSquares[i] = 'X'; - setSquares(nextSquares); - } - - return ( - <> -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - -Now that your state handling is in the `Board` component, the parent `Board` component passes props to the child `Square` components so that they can be displayed correctly. When clicking on a `Square`, the child `Square` component now asks the parent `Board` component to update the state of the board. When the `Board`'s state changes, both the `Board` component and every child `Square` re-renders automatically. Keeping the state of all squares in the `Board` component will allow it to determine the winner in the future. - -Let's recap what happens when a user clicks the top left square on your board to add an `X` to it: - -1. Clicking on the upper left square runs the function that the `button` received as its `onClick` prop from the `Square`. The `Square` component received that function as its `onSquareClick` prop from the `Board`. The `Board` component defined that function directly in the JSX. It calls `handleClick` with an argument of `0`. -1. `handleClick` uses the argument (`0`) to update the first element of the `squares` array from `null` to `X`. -1. The `squares` state of the `Board` component was updated, so the `Board` and all of its children re-render. This causes the `value` prop of the `Square` component with index `0` to change from `null` to `X`. - -In the end the user sees that the upper left square has changed from empty to having an `X` after clicking it. - - - -The DOM `; +} +``` + +At this point you should see an empty tic-tac-toe board: + +![empty board](../images/tutorial/empty-board.png) + +And your code should look like this: + + + +```js src/App.js +import { useState } from 'react'; + +function Square({ value }) { + return ; +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + return ( + <> +
    + + + +
    +
    + + + +
    +
    + + + +
    + + ); +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + +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` is clicked. The `Board` component now maintains which squares are filled. You'll need to create a way for the `Square` to update the `Board`'s state. Since state is private to a component that defines it, you cannot update the `Board`'s state directly from `Square`. + +Instead, you'll pass down a function from the `Board` component to the `Square` component, and you'll have `Square` 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`: + +```js {3} +function Square({ value }) { + return ( + + ); +} +``` + +Next, you'll add the `onSquareClick` function to the `Square` component's props: + +```js {1} +function Square({ value, onSquareClick }) { + return ( + + ); +} +``` + +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 a function to the `onSquareClick` prop of the first `Square` component: + +```js {7} +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + + return ( + <> +
    + + //... + ); +} +``` + +Lastly, you will define the `handleClick` function inside the Board component to update the `squares` array holding your board's state: + +```js {4-8} +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick() { + const nextSquares = squares.slice(); + nextSquares[0] = "X"; + setSquares(nextSquares); + } + + return ( + // ... + ) +} +``` + +The `handleClick` function creates a copy of the `squares` array (`nextSquares`) with the JavaScript `slice()` Array method. Then, `handleClick` updates the `nextSquares` array to add `X` to the first (`[0]` index) square. + +Calling the `setSquares` function lets React know the state of the component has changed. This will trigger a re-render of the components that use the `squares` state (`Board`) as well as its child components (the `Square` components that make up the board). + + + +JavaScript supports [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) which means an inner function (e.g. `handleClick`) has access to variables and functions defined in an outer function (e.g. `Board`). The `handleClick` function can read the `squares` state and call the `setSquares` method because they are both defined inside of the `Board` function. + + + +Now you can add X's to the board... but only to the upper left square. Your `handleClick` function is hardcoded to update the index for the upper left square (`0`). Let's update `handleClick` to be able to update any square. Add an argument `i` to the `handleClick` function that takes the index of the square to update: + +```js {4,6} +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + const nextSquares = squares.slice(); + nextSquares[i] = "X"; + setSquares(nextSquares); + } + + return ( + // ... + ) +} +``` + +Next, you will need to pass that `i` to `handleClick`. You could try to set the `onSquareClick` prop of square to be `handleClick(0)` directly in the JSX like this, but it won't work: + +```jsx + +``` + +Here is why this doesn't work. The `handleClick(0)` call will be a part of rendering the board component. Because `handleClick(0)` alters the state of the board component by calling `setSquares`, your entire board component will be re-rendered again. But this runs `handleClick(0)` again, leading to an infinite loop: + + + +Too many re-renders. React limits the number of renders to prevent an infinite loop. + + + +Why didn't this problem happen earlier? + +When you were passing `onSquareClick={handleClick}`, you were passing the `handleClick` function down as a prop. You were not calling it! But now you are *calling* that function right away--notice the parentheses in `handleClick(0)`--and that's why it runs too early. You don't *want* to call `handleClick` until the user clicks! + +You could fix this by creating a function like `handleFirstSquareClick` that calls `handleClick(0)`, a function like `handleSecondSquareClick` that calls `handleClick(1)`, and so on. You would pass (rather than call) these functions down as props like `onSquareClick={handleFirstSquareClick}`. This would solve the infinite loop. + +However, defining nine different functions and giving each of them a name is too verbose. Instead, let's do this: + +```js {6} +export default function Board() { + // ... + return ( + <> +
    + handleClick(0)} /> + // ... + ); +} +``` + +Notice the new `() =>` syntax. Here, `() => handleClick(0)` is an *arrow function,* which is a shorter way to define functions. When the square is clicked, the code after the `=>` "arrow" will run, calling `handleClick(0)`. + +Now you need to update the other eight squares to call `handleClick` from the arrow functions you pass. Make sure that the argument for each call of the `handleClick` corresponds to the index of the correct square: + +```js {6-8,11-13,16-18} +export default function Board() { + // ... + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +}; +``` + +Now you can again add X's to any square on the board by clicking on them: + +![filling the board with X](../images/tutorial/tictac-adding-x-s.gif) + +But this time all the state management is handled by the `Board` component! + +This is what your code should look like: + + + +```js src/App.js +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +export default function Board() { + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + const nextSquares = squares.slice(); + nextSquares[i] = 'X'; + setSquares(nextSquares); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + +Now that your state handling is in the `Board` component, the parent `Board` component passes props to the child `Square` components so that they can be displayed correctly. When clicking on a `Square`, the child `Square` component now asks the parent `Board` component to update the state of the board. When the `Board`'s state changes, both the `Board` component and every child `Square` re-renders automatically. Keeping the state of all squares in the `Board` component will allow it to determine the winner in the future. + +Let's recap what happens when a user clicks the top left square on your board to add an `X` to it: + +1. Clicking on the upper left square runs the function that the `button` received as its `onClick` prop from the `Square`. The `Square` component received that function as its `onSquareClick` prop from the `Board`. The `Board` component defined that function directly in the JSX. It calls `handleClick` with an argument of `0`. +1. `handleClick` uses the argument (`0`) to update the first element of the `squares` array from `null` to `X`. +1. The `squares` state of the `Board` component was updated, so the `Board` and all of its children re-render. This causes the `value` prop of the `Square` component with index `0` to change from `null` to `X`. + +In the end the user sees that the upper left square has changed from empty to having an `X` after clicking it. + + + +The DOM ` - ); -} - -export default function Board() { - const [xIsNext, setXIsNext] = useState(true); - const [squares, setSquares] = useState(Array(9).fill(null)); - - function handleClick(i) { - if (squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = 'X'; - } else { - nextSquares[i] = 'O'; - } - setSquares(nextSquares); - setXIsNext(!xIsNext); - } - - return ( - <> -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - - - + +It's now time to fix a major defect in this tic-tac-toe game: the "O"s cannot be marked 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 Board component: + +```js {2} +function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + // ... +} +``` + +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`: + +```js {7,8,9,10,11,13} +export default function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = "X"; + } else { + nextSquares[i] = "O"; + } + setSquares(nextSquares); + setXIsNext(!xIsNext); + } + + return ( + //... + ); +} +``` + +Now, as you click on different squares, they will alternate between `X` and `O`, as they should! + +But wait, there's a problem. Try clicking on the same square multiple times: + +![O overwriting an X](../images/tutorial/o-replaces-x.gif) + +The `X` is overwritten by an `O`! While this would add a very interesting twist to the game, we're going to stick to the original rules for now. + +When you mark a square with an `X` or an `O` you aren't first checking to see if the square already has an `X` or `O` value. You can fix this by *returning early*. You'll check to see if the square already has an `X` or an `O`. If the square is already filled, you will `return` in the `handleClick` function early--before it tries to update the board state. + +```js {2,3,4} +function handleClick(i) { + if (squares[i]) { + return; + } + const nextSquares = squares.slice(); + //... +} +``` + +Now you can only add `X`'s or `O`'s to empty squares! Here is what your code should look like at this point: + + + +```js src/App.js +import { useState } from 'react'; + +function Square({value, onSquareClick}) { + return ( + + ); +} + +export default function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + if (squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = 'X'; + } else { + nextSquares[i] = 'O'; + } + setSquares(nextSquares); + setXIsNext(!xIsNext); + } + + return ( + <> +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + ### Declaring a winner {/*declaring-a-winner*/} - -Now that the players can take turns, you'll want to show when the game is won and there are no more turns to make. To do this you'll add a helper function called `calculateWinner` that takes an array of 9 squares, checks for a winner and returns `'X'`, `'O'`, or `null` as appropriate. Don't worry too much about the `calculateWinner` function; it's not specific to React: - -```js src/App.js -export default function Board() { - //... -} - -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; -} -``` - - - -It does not matter whether you define `calculateWinner` before or after the `Board`. Let's put it at the end so that you don't have to scroll past it every time you edit your components. - - - -You will call `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 an `X` or an `O`. We'd like to return early in both cases: - -```js {2} -function handleClick(i) { - if (squares[i] || calculateWinner(squares)) { - return; - } - const nextSquares = squares.slice(); - //... -} -``` - -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 if the game is over and if the game is ongoing you'll display which player's turn is next: - -```js {3-9,13} -export default function Board() { - // ... - const winner = calculateWinner(squares); - let status; - if (winner) { - status = "Winner: " + winner; - } else { - status = "Next player: " + (xIsNext ? "X" : "O"); - } - - return ( - <> -
    {status}
    -
    - // ... - ) -} -``` - -Congratulations! You now have a working tic-tac-toe game. And you've just learned the basics of React too. So _you_ are the real winner here. Here is what the code should look like: - - - -```js src/App.js -import { useState } from 'react'; - -function Square({value, onSquareClick}) { - return ( - - ); -} - -export default function Board() { - const [xIsNext, setXIsNext] = useState(true); - const [squares, setSquares] = useState(Array(9).fill(null)); - - function handleClick(i) { - if (calculateWinner(squares) || squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = 'X'; - } else { - nextSquares[i] = 'O'; - } - setSquares(nextSquares); - setXIsNext(!xIsNext); - } - - const winner = calculateWinner(squares); - let status; - if (winner) { - status = 'Winner: ' + winner; - } else { - status = 'Next player: ' + (xIsNext ? 'X' : 'O'); - } - - return ( - <> -
    {status}
    -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -} - -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; -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - + +Now that the players can take turns, you'll want to show when the game is won and there are no more turns to make. To do this you'll add a helper function called `calculateWinner` that takes an array of 9 squares, checks for a winner and returns `'X'`, `'O'`, or `null` as appropriate. Don't worry too much about the `calculateWinner` function; it's not specific to React: + +```js src/App.js +export default function Board() { + //... +} + +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; +} +``` + + + +It does not matter whether you define `calculateWinner` before or after the `Board`. Let's put it at the end so that you don't have to scroll past it every time you edit your components. + + + +You will call `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 an `X` or an `O`. We'd like to return early in both cases: + +```js {2} +function handleClick(i) { + if (squares[i] || calculateWinner(squares)) { + return; + } + const nextSquares = squares.slice(); + //... +} +``` + +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 if the game is over and if the game is ongoing you'll display which player's turn is next: + +```js {3-9,13} +export default function Board() { + // ... + const winner = calculateWinner(squares); + let status; + if (winner) { + status = "Winner: " + winner; + } else { + status = "Next player: " + (xIsNext ? "X" : "O"); + } + + return ( + <> +
    {status}
    +
    + // ... + ) +} +``` + +Congratulations! You now have a working tic-tac-toe game. And you've just learned the basics of React too. So _you_ are the real winner here. Here is what the code should look like: + + + +```js src/App.js +import { useState } from 'react'; + +function Square({value, onSquareClick}) { + return ( + + ); +} + +export default function Board() { + const [xIsNext, setXIsNext] = useState(true); + const [squares, setSquares] = useState(Array(9).fill(null)); + + function handleClick(i) { + if (calculateWinner(squares) || squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = 'X'; + } else { + nextSquares[i] = 'O'; + } + setSquares(nextSquares); + setXIsNext(!xIsNext); + } + + const winner = calculateWinner(squares); + let status; + if (winner) { + status = 'Winner: ' + winner; + } else { + status = 'Next player: ' + (xIsNext ? 'X' : 'O'); + } + + return ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +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; +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + ## Adding time travel {/*adding-time-travel*/} - -As a final exercise, let's make it possible to "go back in time" to the previous moves in the game. - + +As a final exercise, let's make it possible to "go back in time" to the previous moves in the game. + ### Storing a history of moves {/*storing-a-history-of-moves*/} - -If you mutated the `squares` array, implementing time travel would be very difficult. - -However, you used `slice()` to create a new copy of the `squares` array after every move, and treated it as immutable. This will allow you to store every past version of the `squares` array, and navigate between the turns that have already happened. - -You'll store the past `squares` arrays in another array called `history`, which you'll store as a new state variable. The `history` array represents all board states, from the first to the last move, and has a shape like this: - -```jsx -[ - // Before first move - [null, null, null, null, null, null, null, null, null], - // After first move - [null, null, null, null, 'X', null, null, null, null], - // After second move - [null, null, null, null, 'X', null, null, null, 'O'], - // ... -] -``` - + +If you mutated the `squares` array, implementing time travel would be very difficult. + +However, you used `slice()` to create a new copy of the `squares` array after every move, and treated it as immutable. This will allow you to store every past version of the `squares` array, and navigate between the turns that have already happened. + +You'll store the past `squares` arrays in another array called `history`, which you'll store as a new state variable. The `history` array represents all board states, from the first to the last move, and has a shape like this: + +```jsx +[ + // Before first move + [null, null, null, null, null, null, null, null, null], + // After first move + [null, null, null, null, 'X', null, null, null, null], + // After second move + [null, null, null, null, 'X', null, null, null, 'O'], + // ... +] +``` + ### Lifting state up, again {/*lifting-state-up-again*/} - -You will now write a new top-level component called `Game` to display a list of past moves. That's where you will place the `history` state that contains the entire game history. - -Placing the `history` state into the `Game` component will let you remove the `squares` state from its child `Board` component. Just like you "lifted state up" from the `Square` component into the `Board` component, you will now lift it up from the `Board` into the top-level `Game` component. This gives the `Game` component full control over the `Board`'s data and lets it instruct the `Board` to render previous turns from the `history`. - -First, add a `Game` component with `export default`. Have it render the `Board` component and some markup: - -```js {1,5-16} -function Board() { - // ... -} - -export default function Game() { - return ( -
    -
    - -
    -
    -
      {/*TODO*/}
    -
    -
    - ); -} -``` - -Note that you are removing the `export default` keywords before the `function Board() {` declaration and adding them before the `function Game() {` declaration. This tells your `index.js` file to use the `Game` component as the top-level component instead of your `Board` component. The additional `div`s returned by the `Game` component are making room for the game information you'll add to the board later. - -Add some state to the `Game` component to track which player is next and the history of moves: - -```js {2-3} -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - // ... -``` - -Notice how `[Array(9).fill(null)]` is an array with a single item, which itself is an array of 9 `null`s. - -To render the squares for the current move, you'll want to read the last squares array from the `history`. You don't need `useState` for this--you already have enough information to calculate it during rendering: - -```js {4} -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - const currentSquares = history[history.length - 1]; - // ... -``` - -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: - -```js {6-8,13} -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - const currentSquares = history[history.length - 1]; - - function handlePlay(nextSquares) { - // TODO - } - - return ( -
    -
    - - //... - ) -} -``` - -Let's make the `Board` component fully controlled by the props it receives. Change the `Board` component to take three props: `xIsNext`, `squares`, and a new `onPlay` function that `Board` can call with the updated squares array when a player makes a move. Next, remove the first two lines of the `Board` function that call `useState`: - -```js {1} -function Board({ xIsNext, squares, onPlay }) { - function handleClick(i) { - //... - } - // ... -} -``` - -Now replace the `setSquares` and `setXIsNext` calls in `handleClick` in the `Board` component with a single call to your new `onPlay` function so the `Game` component can update the `Board` when the user clicks a square: - -```js {12} -function Board({ xIsNext, squares, onPlay }) { - function handleClick(i) { - if (calculateWinner(squares) || squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = "X"; - } else { - nextSquares[i] = "O"; - } - onPlay(nextSquares); - } - //... -} -``` - -The `Board` component is fully controlled by the props passed to it by the `Game` component. You need to implement the `handlePlay` function in the `Game` component to get the game working again. - -What should `handlePlay` do when called? Remember that Board used to call `setSquares` with an updated array; now it passes the updated `squares` array to `onPlay`. - -The `handlePlay` function needs to update `Game`'s state to trigger a re-render, but you don't have a `setSquares` function that you can call any more--you're now using the `history` state variable to store this information. You'll want to update `history` by appending the updated `squares` array as a new history entry. You also want to toggle `xIsNext`, just as Board used to do: - -```js {4-5} -export default function Game() { - //... - function handlePlay(nextSquares) { - setHistory([...history, nextSquares]); - setXIsNext(!xIsNext); - } - //... -} -``` - -Here, `[...history, nextSquares]` creates a new array that contains all the items in `history`, followed by `nextSquares`. (You can read the `...history` [*spread syntax*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) as "enumerate all the items in `history`".) - -For example, if `history` is `[[null,null,null], ["X",null,null]]` and `nextSquares` is `["X",null,"O"]`, then the new `[...history, nextSquares]` array will be `[[null,null,null], ["X",null,null], ["X",null,"O"]]`. - -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: - - - -```js src/App.js -import { useState } from 'react'; - -function Square({ value, onSquareClick }) { - return ( - - ); -} - -function Board({ xIsNext, squares, onPlay }) { - function handleClick(i) { - if (calculateWinner(squares) || squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = 'X'; - } else { - nextSquares[i] = 'O'; - } - onPlay(nextSquares); - } - - const winner = calculateWinner(squares); - let status; - if (winner) { - status = 'Winner: ' + winner; - } else { - status = 'Next player: ' + (xIsNext ? 'X' : 'O'); - } - - return ( - <> -
    {status}
    -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -} - -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - const currentSquares = history[history.length - 1]; - - function handlePlay(nextSquares) { - setHistory([...history, 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; -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - + +You will now write a new top-level component called `Game` to display a list of past moves. That's where you will place the `history` state that contains the entire game history. + +Placing the `history` state into the `Game` component will let you remove the `squares` state from its child `Board` component. Just like you "lifted state up" from the `Square` component into the `Board` component, you will now lift it up from the `Board` into the top-level `Game` component. This gives the `Game` component full control over the `Board`'s data and lets it instruct the `Board` to render previous turns from the `history`. + +First, add a `Game` component with `export default`. Have it render the `Board` component and some markup: + +```js {1,5-16} +function Board() { + // ... +} + +export default function Game() { + return ( +
    +
    + +
    +
    +
      {/*TODO*/}
    +
    +
    + ); +} +``` + +Note that you are removing the `export default` keywords before the `function Board() {` declaration and adding them before the `function Game() {` declaration. This tells your `index.js` file to use the `Game` component as the top-level component instead of your `Board` component. The additional `div`s returned by the `Game` component are making room for the game information you'll add to the board later. + +Add some state to the `Game` component to track which player is next and the history of moves: + +```js {2-3} +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + // ... +``` + +Notice how `[Array(9).fill(null)]` is an array with a single item, which itself is an array of 9 `null`s. + +To render the squares for the current move, you'll want to read the last squares array from the `history`. You don't need `useState` for this--you already have enough information to calculate it during rendering: + +```js {4} +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const currentSquares = history[history.length - 1]; + // ... +``` + +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: + +```js {6-8,13} +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const currentSquares = history[history.length - 1]; + + function handlePlay(nextSquares) { + // TODO + } + + return ( +
    +
    + + //... + ) +} +``` + +Let's make the `Board` component fully controlled by the props it receives. Change the `Board` component to take three props: `xIsNext`, `squares`, and a new `onPlay` function that `Board` can call with the updated squares array when a player makes a move. Next, remove the first two lines of the `Board` function that call `useState`: + +```js {1} +function Board({ xIsNext, squares, onPlay }) { + function handleClick(i) { + //... + } + // ... +} +``` + +Now replace the `setSquares` and `setXIsNext` calls in `handleClick` in the `Board` component with a single call to your new `onPlay` function so the `Game` component can update the `Board` when the user clicks a square: + +```js {12} +function Board({ xIsNext, squares, onPlay }) { + function handleClick(i) { + if (calculateWinner(squares) || squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = "X"; + } else { + nextSquares[i] = "O"; + } + onPlay(nextSquares); + } + //... +} +``` + +The `Board` component is fully controlled by the props passed to it by the `Game` component. You need to implement the `handlePlay` function in the `Game` component to get the game working again. + +What should `handlePlay` do when called? Remember that Board used to call `setSquares` with an updated array; now it passes the updated `squares` array to `onPlay`. + +The `handlePlay` function needs to update `Game`'s state to trigger a re-render, but you don't have a `setSquares` function that you can call any more--you're now using the `history` state variable to store this information. You'll want to update `history` by appending the updated `squares` array as a new history entry. You also want to toggle `xIsNext`, just as Board used to do: + +```js {4-5} +export default function Game() { + //... + function handlePlay(nextSquares) { + setHistory([...history, nextSquares]); + setXIsNext(!xIsNext); + } + //... +} +``` + +Here, `[...history, nextSquares]` creates a new array that contains all the items in `history`, followed by `nextSquares`. (You can read the `...history` [*spread syntax*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) as "enumerate all the items in `history`".) + +For example, if `history` is `[[null,null,null], ["X",null,null]]` and `nextSquares` is `["X",null,"O"]`, then the new `[...history, nextSquares]` array will be `[[null,null,null], ["X",null,null], ["X",null,"O"]]`. + +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: + + + +```js src/App.js +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +function Board({ xIsNext, squares, onPlay }) { + function handleClick(i) { + if (calculateWinner(squares) || squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = 'X'; + } else { + nextSquares[i] = 'O'; + } + onPlay(nextSquares); + } + + const winner = calculateWinner(squares); + let status; + if (winner) { + status = 'Winner: ' + winner; + } else { + status = 'Next player: ' + (xIsNext ? 'X' : 'O'); + } + + return ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const currentSquares = history[history.length - 1]; + + function handlePlay(nextSquares) { + setHistory([...history, 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; +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + ### Showing the past moves {/*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. - -React elements like ` - - ); - }); - - return ( -
    -
    - -
    -
    -
      {moves}
    -
    -
    - ); -} -``` - -You can see what your code should look like below. Note that you should see an error in the developer tools console that says: - - -Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Game`. - - -You'll fix this error in the next section. - - - -```js src/App.js -import { useState } from 'react'; - -function Square({ value, onSquareClick }) { - return ( - - ); -} - -function Board({ xIsNext, squares, onPlay }) { - function handleClick(i) { - if (calculateWinner(squares) || squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = 'X'; - } else { - nextSquares[i] = 'O'; - } - onPlay(nextSquares); - } - - const winner = calculateWinner(squares); - let status; - if (winner) { - status = 'Winner: ' + winner; - } else { - status = 'Next player: ' + (xIsNext ? 'X' : 'O'); - } - - return ( - <> -
    {status}
    -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -} - -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - const currentSquares = history[history.length - 1]; - - function handlePlay(nextSquares) { - setHistory([...history, nextSquares]); - setXIsNext(!xIsNext); - } - - function jumpTo(nextMove) { - // TODO - } - - const moves = history.map((squares, move) => { - let description; - if (move > 0) { - description = 'Go to move #' + move; - } else { - description = 'Go to game start'; - } - return ( -
  • - -
  • - ); - }); - - return ( -
    -
    - -
    -
    -
      {moves}
    -
    -
    - ); -} - -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; -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} - -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - -As you iterate through the `history` array inside the function you passed to `map`, the `squares` argument goes through each element of `history`, and the `move` argument goes through each array index: `0`, `1`, `2`, …. (In most cases, you'd need the actual array elements, but to render a list of moves you will only need indexes.) - -For each move in the tic-tac-toe game's history, you create a list item `
  • ` which contains a button ` +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} +``` + +You can see what your code should look like below. Note that you should see an error in the developer tools console that says: + + +Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Game`. + + +You'll fix this error in the next section. + + + +```js src/App.js +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +function Board({ xIsNext, squares, onPlay }) { + function handleClick(i) { + if (calculateWinner(squares) || squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = 'X'; + } else { + nextSquares[i] = 'O'; + } + onPlay(nextSquares); + } + + const winner = calculateWinner(squares); + let status; + if (winner) { + status = 'Winner: ' + winner; + } else { + status = 'Next player: ' + (xIsNext ? 'X' : 'O'); + } + + return ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const currentSquares = history[history.length - 1]; + + function handlePlay(nextSquares) { + setHistory([...history, nextSquares]); + setXIsNext(!xIsNext); + } + + function jumpTo(nextMove) { + // TODO + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} + +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + +As you iterate through the `history` array inside the function you passed to `map`, the `squares` argument goes through each element of `history`, and the `move` argument goes through each array index: `0`, `1`, `2`, …. (In most cases, you'd need the actual array elements, but to render a list of moves you will only need indexes.) + +For each move in the tic-tac-toe game's history, you create a list item `
  • ` which contains a button ` -
  • - ); -}); -``` - - - -```js src/App.js -import { useState } from 'react'; - -function Square({ value, onSquareClick }) { - return ( - - ); -} - -function Board({ xIsNext, squares, onPlay }) { - function handleClick(i) { - if (calculateWinner(squares) || squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = 'X'; - } else { - nextSquares[i] = 'O'; - } - onPlay(nextSquares); - } - - const winner = calculateWinner(squares); - let status; - if (winner) { - status = 'Winner: ' + winner; - } else { - status = 'Next player: ' + (xIsNext ? 'X' : 'O'); - } - - return ( - <> -
    {status}
    -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -} - -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - const currentSquares = history[history.length - 1]; - - function handlePlay(nextSquares) { - setHistory([...history, nextSquares]); - setXIsNext(!xIsNext); - } - - function jumpTo(nextMove) { - // TODO - } - - const moves = history.map((squares, move) => { - let description; - if (move > 0) { - description = 'Go to move #' + move; - } else { - description = 'Go to game start'; - } - return ( -
  • - -
  • - ); - }); - - return ( -
    -
    - -
    -
    -
      {moves}
    -
    -
    - ); -} - -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; -} - -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} - -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - -Before you can implement `jumpTo`, 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`, defaulting to `0`: - -```js {4} -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - const [currentMove, setCurrentMove] = useState(0); - const currentSquares = history[history.length - 1]; - //... -} -``` - -Next, update the `jumpTo` function inside `Game` to update that `currentMove`. You'll also set `xIsNext` to `true` if the number that you're changing `currentMove` to is even. - -```js {4-5} -export default function Game() { - // ... - function jumpTo(nextMove) { - setCurrentMove(nextMove); - setXIsNext(nextMove % 2 === 0); - } - //... -} -``` - -You will now make two changes to the `Game`'s `handlePlay` function 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 (`...` spread syntax) in `history`, you'll add it after all items in `history.slice(0, currentMove + 1)` so that you're only keeping that portion of the old history. -- Each time a move is made, you need to update `currentMove` to point to the latest history entry. - -```js {2-4} -function handlePlay(nextSquares) { - const nextHistory = [...history.slice(0, currentMove + 1), 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: - -```js {5} -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - const [currentMove, setCurrentMove] = useState(0); - const currentSquares = history[currentMove]; - - // ... -} -``` - -If you click on any step in the game's history, the tic-tac-toe board should immediately update to show what the board looked like after that step occurred. - - - -```js src/App.js -import { useState } from 'react'; - -function Square({value, onSquareClick}) { - return ( - - ); -} - -function Board({ xIsNext, squares, onPlay }) { - function handleClick(i) { - if (calculateWinner(squares) || squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = 'X'; - } else { - nextSquares[i] = 'O'; - } - onPlay(nextSquares); - } - - const winner = calculateWinner(squares); - let status; - if (winner) { - status = 'Winner: ' + winner; - } else { - status = 'Next player: ' + (xIsNext ? 'X' : 'O'); - } - - return ( - <> -
    {status}
    -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -} - -export default function Game() { - const [xIsNext, setXIsNext] = useState(true); - const [history, setHistory] = useState([Array(9).fill(null)]); - const [currentMove, setCurrentMove] = useState(0); - const currentSquares = history[currentMove]; - - function handlePlay(nextSquares) { - const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; - setHistory(nextHistory); - setCurrentMove(nextHistory.length - 1); - setXIsNext(!xIsNext); - } - - function jumpTo(nextMove) { - setCurrentMove(nextMove); - setXIsNext(nextMove % 2 === 0); - } - - const moves = history.map((squares, move) => { - let description; - if (move > 0) { - description = 'Go to move #' + move; - } else { - description = 'Go to game start'; - } - return ( -
  • - -
  • - ); - }); - - return ( -
    -
    - -
    -
    -
      {moves}
    -
    -
    - ); -} - -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; -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - + +In the tic-tac-toe game's history, each past move has a unique ID associated with it: it's the sequential number of the move. Moves will never be re-ordered, deleted, or inserted in the middle, so it's safe to use the move index as a key. + +In the `Game` function, you can add the key as `
  • `, and if you reload the rendered game, React's "key" error should disappear: + +```js {4} +const moves = history.map((squares, move) => { + //... + return ( +
  • + +
  • + ); +}); +``` + + + +```js src/App.js +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +function Board({ xIsNext, squares, onPlay }) { + function handleClick(i) { + if (calculateWinner(squares) || squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = 'X'; + } else { + nextSquares[i] = 'O'; + } + onPlay(nextSquares); + } + + const winner = calculateWinner(squares); + let status; + if (winner) { + status = 'Winner: ' + winner; + } else { + status = 'Next player: ' + (xIsNext ? 'X' : 'O'); + } + + return ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const currentSquares = history[history.length - 1]; + + function handlePlay(nextSquares) { + setHistory([...history, nextSquares]); + setXIsNext(!xIsNext); + } + + function jumpTo(nextMove) { + // TODO + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} + +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} + +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + +Before you can implement `jumpTo`, 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`, defaulting to `0`: + +```js {4} +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + const currentSquares = history[history.length - 1]; + //... +} +``` + +Next, update the `jumpTo` function inside `Game` to update that `currentMove`. You'll also set `xIsNext` to `true` if the number that you're changing `currentMove` to is even. + +```js {4-5} +export default function Game() { + // ... + function jumpTo(nextMove) { + setCurrentMove(nextMove); + setXIsNext(nextMove % 2 === 0); + } + //... +} +``` + +You will now make two changes to the `Game`'s `handlePlay` function 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 (`...` spread syntax) in `history`, you'll add it after all items in `history.slice(0, currentMove + 1)` so that you're only keeping that portion of the old history. +- Each time a move is made, you need to update `currentMove` to point to the latest history entry. + +```js {2-4} +function handlePlay(nextSquares) { + const nextHistory = [...history.slice(0, currentMove + 1), 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: + +```js {5} +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + const currentSquares = history[currentMove]; + + // ... +} +``` + +If you click on any step in the game's history, the tic-tac-toe board should immediately update to show what the board looked like after that step occurred. + + + +```js src/App.js +import { useState } from 'react'; + +function Square({value, onSquareClick}) { + return ( + + ); +} + +function Board({ xIsNext, squares, onPlay }) { + function handleClick(i) { + if (calculateWinner(squares) || squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = 'X'; + } else { + nextSquares[i] = 'O'; + } + onPlay(nextSquares); + } + + const winner = calculateWinner(squares); + let status; + if (winner) { + status = 'Winner: ' + winner; + } else { + status = 'Next player: ' + (xIsNext ? 'X' : 'O'); + } + + return ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [xIsNext, setXIsNext] = useState(true); + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + const currentSquares = history[currentMove]; + + function handlePlay(nextSquares) { + const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; + setHistory(nextHistory); + setCurrentMove(nextHistory.length - 1); + setXIsNext(!xIsNext); + } + + function jumpTo(nextMove) { + setCurrentMove(nextMove); + setXIsNext(nextMove % 2 === 0); + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + ### Final cleanup {/*final-cleanup*/} - -If you look at the code very closely, you may notice that `xIsNext === true` when `currentMove` is even and `xIsNext === false` when `currentMove` is odd. In other words, if you know the value of `currentMove`, then you can always figure out what `xIsNext` should be. - -There's no reason for you to store both of these in state. In fact, always try to avoid redundant state. Simplifying what you store in state reduces bugs and makes your code easier to understand. Change `Game` so that it doesn't store `xIsNext` as a separate state variable and instead figures it out based on the `currentMove`: - -```js {4,11,15} -export default function Game() { - const [history, setHistory] = useState([Array(9).fill(null)]); - const [currentMove, setCurrentMove] = useState(0); - 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); - } - // ... -} -``` - -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. - + +If you look at the code very closely, you may notice that `xIsNext === true` when `currentMove` is even and `xIsNext === false` when `currentMove` is odd. In other words, if you know the value of `currentMove`, then you can always figure out what `xIsNext` should be. + +There's no reason for you to store both of these in state. In fact, always try to avoid redundant state. Simplifying what you store in state reduces bugs and makes your code easier to understand. Change `Game` so that it doesn't store `xIsNext` as a separate state variable and instead figures it out based on the `currentMove`: + +```js {4,11,15} +export default function Game() { + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + 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); + } + // ... +} +``` + +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 {/*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, -- 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 works. - -Check out the final result here: - - - -```js src/App.js -import { useState } from 'react'; - -function Square({ value, onSquareClick }) { - return ( - - ); -} - -function Board({ xIsNext, squares, onPlay }) { - function handleClick(i) { - if (calculateWinner(squares) || squares[i]) { - return; - } - const nextSquares = squares.slice(); - if (xIsNext) { - nextSquares[i] = 'X'; - } else { - nextSquares[i] = 'O'; - } - onPlay(nextSquares); - } - - const winner = calculateWinner(squares); - let status; - if (winner) { - status = 'Winner: ' + winner; - } else { - status = 'Next player: ' + (xIsNext ? 'X' : 'O'); - } - - return ( - <> -
    {status}
    -
    - handleClick(0)} /> - handleClick(1)} /> - handleClick(2)} /> -
    -
    - handleClick(3)} /> - handleClick(4)} /> - handleClick(5)} /> -
    -
    - handleClick(6)} /> - handleClick(7)} /> - handleClick(8)} /> -
    - - ); -} - -export default function Game() { - const [history, setHistory] = useState([Array(9).fill(null)]); - const [currentMove, setCurrentMove] = useState(0); - 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); - } - - const moves = history.map((squares, move) => { - let description; - if (move > 0) { - description = 'Go to move #' + move; - } else { - description = 'Go to game start'; - } - return ( -
  • - -
  • - ); - }); - - return ( -
    -
    - -
    -
    -
      {moves}
    -
    -
    - ); -} - -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; -} -``` - -```css src/styles.css -* { - box-sizing: border-box; -} - -body { - font-family: sans-serif; - margin: 20px; - padding: 0; -} - -.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; -} - -.board-row:after { - clear: both; - content: ''; - display: table; -} - -.status { - margin-bottom: 10px; -} -.game { - display: flex; - flex-direction: row; -} - -.game-info { - margin-left: 20px; -} -``` - -
    - -If you have extra time or want to practice your new React skills, here are some ideas for improvements that you could make to the tic-tac-toe game, listed in order of increasing difficulty: - -1. For the current move only, show "You are at move #..." instead of a button. -1. Rewrite `Board` to use two loops to make the squares instead of hardcoding them. -1. Add a toggle button that lets you sort the moves in either ascending or descending order. -1. When someone wins, highlight the three squares that caused the win (and when no one wins, display a message about the result being a draw). -1. Display the location for each move in the format (row, col) in the move history list. - -Throughout this tutorial, you've touched on React concepts including elements, components, props, and state. Now that you've seen how these concepts work when building a game, check out [Thinking in React](/learn/thinking-in-react) to see how the same React concepts work when building an app's UI. + +Congratulations! You've created a tic-tac-toe game that: + +- Lets you play tic-tac-toe, +- Indicates when a player has won the game, +- 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 works. + +Check out the final result here: + + + +```js src/App.js +import { useState } from 'react'; + +function Square({ value, onSquareClick }) { + return ( + + ); +} + +function Board({ xIsNext, squares, onPlay }) { + function handleClick(i) { + if (calculateWinner(squares) || squares[i]) { + return; + } + const nextSquares = squares.slice(); + if (xIsNext) { + nextSquares[i] = 'X'; + } else { + nextSquares[i] = 'O'; + } + onPlay(nextSquares); + } + + const winner = calculateWinner(squares); + let status; + if (winner) { + status = 'Winner: ' + winner; + } else { + status = 'Next player: ' + (xIsNext ? 'X' : 'O'); + } + + return ( + <> +
    {status}
    +
    + handleClick(0)} /> + handleClick(1)} /> + handleClick(2)} /> +
    +
    + handleClick(3)} /> + handleClick(4)} /> + handleClick(5)} /> +
    +
    + handleClick(6)} /> + handleClick(7)} /> + handleClick(8)} /> +
    + + ); +} + +export default function Game() { + const [history, setHistory] = useState([Array(9).fill(null)]); + const [currentMove, setCurrentMove] = useState(0); + 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); + } + + const moves = history.map((squares, move) => { + let description; + if (move > 0) { + description = 'Go to move #' + move; + } else { + description = 'Go to game start'; + } + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
    +
      {moves}
    +
    +
    + ); +} + +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; +} +``` + +```css src/styles.css +* { + box-sizing: border-box; +} + +body { + font-family: sans-serif; + margin: 20px; + padding: 0; +} + +.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; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} +``` + +
    + +If you have extra time or want to practice your new React skills, here are some ideas for improvements that you could make to the tic-tac-toe game, listed in order of increasing difficulty: + +1. For the current move only, show "You are at move #..." instead of a button. +1. Rewrite `Board` to use two loops to make the squares instead of hardcoding them. +1. Add a toggle button that lets you sort the moves in either ascending or descending order. +1. When someone wins, highlight the three squares that caused the win (and when no one wins, display a message about the result being a draw). +1. Display the location for each move in the format (row, col) in the move history list. + +Throughout this tutorial, you've touched on React concepts including elements, components, props, and state. Now that you've seen how these concepts work when building a game, check out [Thinking in React](/learn/thinking-in-react) to see how the same React concepts work when building an app's UI. From de3b32b7bfd3dbbd43762ea590b3ff527ab82010 Mon Sep 17 00:00:00 2001 From: QuyetNh0905 Date: Tue, 4 Nov 2025 19:37:59 +0700 Subject: [PATCH 2/2] Translate the whole tutorial-tic-tac-toe.md to Vietnamese --- src/content/learn/tutorial-tic-tac-toe.md | 360 +++++++++++----------- 1 file changed, 180 insertions(+), 180 deletions(-) diff --git a/src/content/learn/tutorial-tic-tac-toe.md b/src/content/learn/tutorial-tic-tac-toe.md index ee5b14d60..4f550d173 100644 --- a/src/content/learn/tutorial-tic-tac-toe.md +++ b/src/content/learn/tutorial-tic-tac-toe.md @@ -358,7 +358,7 @@ export default function Square() { } ``` -You'll get this error: +Bạn sẽ gặp lỗi này: @@ -366,7 +366,7 @@ You'll get this error: -React components need to return a single JSX element and not multiple adjacent JSX elements like two buttons. To fix this you can use *Fragments* (`<>` and ``) to wrap multiple adjacent JSX elements like this: +React components cần trả về một phần tử JSX duy nhất và không phải nhiều phần tử JSX liền kề như hai button. Để sửa lỗi này, bạn có thể sử dụng *Fragments* (`<>` và ``) để bọc nhiều phần tử JSX liền kề như sau: ```js {3-6} export default function Square() { @@ -379,17 +379,17 @@ export default function Square() { } ``` -Now you should see: +Bây giờ bạn sẽ thấy: ![two x-filled squares](../images/tutorial/two-x-filled-squares.png) -Great! Now you just need to copy-paste a few times to add nine squares and... +Tuyệt vời! Bây giờ bạn chỉ cần sao chép-dán vài lần để thêm chín ô vuông và... ![nine x-filled squares in a line](../images/tutorial/nine-x-filled-squares.png) -Oh no! The squares are all in a single line, not in a grid like you need for our board. To fix this you'll need to group your squares into rows with `div`s and add some CSS classes. While you're at it, you'll give each square a number to make sure you know where each square is displayed. +Ồ không! Các ô vuông đều nằm trên một dòng duy nhất, không phải trong một lưới như bạn cần cho bảng chơi. Để sửa lỗi này, bạn cần nhóm các ô vuông của mình thành các hàng bằng `div`s và thêm một số lớp CSS. Trong khi làm điều đó, bạn sẽ đặt cho mỗi ô vuông một số để đảm bảo bạn biết mỗi ô vuông được hiển thị ở đâu. -In the `App.js` file, update the `Square` component to look like this: +Trong file `App.js`, cập nhật component `Square` để trông như thế này: ```js {3-19} export default function Square() { @@ -415,11 +415,11 @@ export default function Square() { } ``` -The CSS defined in `styles.css` styles the divs with the `className` of `board-row`. Now that you've grouped your components into rows with the styled `div`s you have your tic-tac-toe board: +CSS được định nghĩa trong `styles.css` tạo kiểu cho các divs có `className` là `board-row`. Bây giờ bạn đã nhóm các component của mình thành các hàng với các `div`s đã được tạo kiểu, bạn đã có bảng chơi tic-tac-toe của mình: ![tic-tac-toe board filled with numbers 1 through 9](../images/tutorial/number-filled-board.png) -But you now have a problem. Your component named `Square`, really isn't a square anymore. Let's fix that by changing the name to `Board`: +Nhưng bây giờ bạn có một vấn đề. Component của bạn có tên `Square`, thực sự không còn là một ô vuông nữa. Hãy sửa điều đó bằng cách đổi tên thành `Board`: ```js {1} export default function Board() { @@ -427,7 +427,7 @@ export default function Board() { } ``` -At this point your code should look something like this: +Ở thời điểm này, code của bạn nên trông giống như thế này: @@ -504,15 +504,15 @@ body { -Psssst... That's a lot to type! It's okay to copy and paste code from this page. However, if you're up for a little challenge, we recommend only copying code that you've manually typed at least once yourself. +Psssst... Đó là rất nhiều để gõ! Không sao nếu bạn sao chép và dán code từ trang này. Tuy nhiên, nếu bạn muốn một chút thử thách, chúng tôi khuyên bạn chỉ sao chép code mà bạn đã tự tay gõ ít nhất một lần. -### Passing data through props {/*passing-data-through-props*/} +### Truyền dữ liệu qua props {/*passing-data-through-props*/} -Next, you'll want to change the value of a square from empty to "X" when the user clicks on the square. With how you've built the board so far you would need to copy-paste the code that updates the square nine times (once for each square you have)! Instead of copy-pasting, React's component architecture allows you to create a reusable component to avoid messy, duplicated code. +Tiếp theo, bạn sẽ muốn thay đổi giá trị của một ô vuông từ trống sang "X" khi người dùng nhấp vào ô vuông đó. Với cách bạn đã xây dựng bảng chơi cho đến nay, bạn sẽ cần sao chép-dán code cập nhật ô vuông chín lần (một lần cho mỗi ô vuông bạn có)! Thay vì sao chép-dán, kiến trúc component của React cho phép bạn tạo một component có thể tái sử dụng để tránh code lộn xộn, trùng lặp. -First, you are going to copy the line defining your first square (``) from your `Board` component into a new `Square` component: +Đầu tiên, bạn sẽ sao chép dòng định nghĩa ô vuông đầu tiên của bạn (``) từ component `Board` của bạn vào một component `Square` mới: ```js {1-3} function Square() { @@ -524,7 +524,7 @@ export default function Board() { } ``` -Then you'll update the Board component to render that `Square` component using JSX syntax: +Sau đó bạn sẽ cập nhật component Board để render component `Square` đó bằng cú pháp JSX: ```js {5-19} // ... @@ -551,15 +551,15 @@ export default function Board() { } ``` -Note how unlike the browser `div`s, your own components `Board` and `Square` must start with a capital letter. +Lưu ý rằng không giống như các `div`s của trình duyệt, các component của riêng bạn `Board` và `Square` phải bắt đầu bằng chữ cái viết hoa. -Let's take a look: +Hãy xem: ![one-filled board](../images/tutorial/board-filled-with-ones.png) -Oh no! You lost the numbered squares you had before. Now each square says "1". To fix this, you will use *props* to pass the value each square should have from the parent component (`Board`) to its child (`Square`). +Ồ không! Bạn đã mất các ô vuông có số mà bạn đã có trước đó. Bây giờ mỗi ô vuông đều hiển thị "1". Để sửa lỗi này, bạn sẽ sử dụng *props* để truyền giá trị mà mỗi ô vuông nên có từ component cha (`Board`) đến component con của nó (`Square`). -Update the `Square` component to read the `value` prop that you'll pass from the `Board`: +Cập nhật component `Square` để đọc prop `value` mà bạn sẽ truyền từ `Board`: ```js {1} function Square({ value }) { @@ -567,9 +567,9 @@ function Square({ value }) { } ``` -`function Square({ value })` indicates the Square component can be passed a prop called `value`. +`function Square({ value })` cho biết component Square có thể được truyền một prop có tên `value`. -Now you want to display that `value` instead of `1` inside every square. Try doing it like this: +Bây giờ bạn muốn hiển thị `value` đó thay vì `1` bên trong mỗi ô vuông. Hãy thử làm như thế này: ```js {2} function Square({ value }) { @@ -577,11 +577,11 @@ function Square({ value }) { } ``` -Oops, this is not what you wanted: +Ồ, đây không phải là điều bạn muốn: ![value-filled board](../images/tutorial/board-filled-with-value.png) -You wanted to render the JavaScript variable called `value` from your component, not the word "value". To "escape into JavaScript" from JSX, you need curly braces. Add curly braces around `value` in JSX like so: +Bạn muốn render biến JavaScript có tên `value` từ component của mình, không phải từ "value". Để "thoát vào JavaScript" từ JSX, bạn cần dấu ngoặc nhọn. Thêm dấu ngoặc nhọn xung quanh `value` trong JSX như sau: ```js {2} function Square({ value }) { @@ -589,11 +589,11 @@ function Square({ value }) { } ``` -For now, you should see an empty board: +Hiện tại, bạn sẽ thấy một bảng trống: ![empty board](../images/tutorial/empty-board.png) -This is because the `Board` component hasn't passed the `value` prop to each `Square` component it renders yet. To fix it you'll add the `value` prop to each `Square` component rendered by the `Board` component: +Điều này là do component `Board` chưa truyền prop `value` cho mỗi component `Square` mà nó render. Để sửa lỗi này, bạn sẽ thêm prop `value` vào mỗi component `Square` được render bởi component `Board`: ```js {5-7,10-12,15-17} export default function Board() { @@ -619,11 +619,11 @@ export default function Board() { } ``` -Now you should see a grid of numbers again: +Bây giờ bạn sẽ lại thấy một lưới số: ![tic-tac-toe board filled with numbers 1 through 9](../images/tutorial/number-filled-board.png) -Your updated code should look like this: +Code đã cập nhật của bạn nên trông như thế này: @@ -702,9 +702,9 @@ body { -### Making an interactive component {/*making-an-interactive-component*/} +### Tạo một component tương tác {/*making-an-interactive-component*/} -Let's fill the `Square` component with an `X` when you click it. Declare a function called `handleClick` inside of the `Square`. Then, add `onClick` to the props of the button JSX element returned from the `Square`: +Hãy điền component `Square` với một `X` khi bạn nhấp vào nó. Khai báo một function có tên `handleClick` bên trong `Square`. Sau đó, thêm `onClick` vào props của phần tử JSX button được trả về từ `Square`: ```js {2-4,9} function Square({ value }) { @@ -723,19 +723,19 @@ function Square({ value }) { } ``` -If you click on a square now, you should see a log saying `"clicked!"` in the _Console_ tab at the bottom of the _Browser_ section in CodeSandbox. Clicking the square more than once will log `"clicked!"` again. Repeated console logs with the same message will not create more lines in the console. Instead, you will see an incrementing counter next to your first `"clicked!"` log. +Nếu bạn nhấp vào một ô vuông bây giờ, bạn sẽ thấy một log nói `"clicked!"` trong tab _Console_ ở cuối phần _Browser_ trong CodeSandbox. Nhấp vào ô vuông nhiều hơn một lần sẽ log `"clicked!"` lại. Các console log lặp lại với cùng một thông báo sẽ không tạo thêm dòng trong console. Thay vào đó, bạn sẽ thấy một bộ đếm tăng dần bên cạnh log `"clicked!"` đầu tiên của bạn. -If you are following this tutorial using your local development environment, you need to open your browser's Console. For example, if you use the Chrome browser, you can view the Console with the keyboard shortcut **Shift + Ctrl + J** (on Windows/Linux) or **Option + ⌘ + J** (on macOS). +Nếu bạn đang theo dõi hướng dẫn này bằng cách sử dụng môi trường phát triển local của mình, bạn cần mở Console của trình duyệt. Ví dụ, nếu bạn sử dụng trình duyệt Chrome, bạn có thể xem Console bằng phím tắt **Shift + Ctrl + J** (trên Windows/Linux) hoặc **Option + ⌘ + J** (trên macOS). -As a next step, you want the Square component to "remember" that it got clicked, and fill it with an "X" mark. To "remember" things, components use *state*. +Như một bước tiếp theo, bạn muốn component Square "nhớ" rằng nó đã được nhấp, và điền nó bằng dấu "X". Để "nhớ" mọi thứ, components sử dụng *state*. -React provides a special function called `useState` that you can call from your component to let it "remember" things. Let's store the current value of the `Square` in state, and change it when the `Square` is clicked. +React cung cấp một function đặc biệt có tên `useState` mà bạn có thể gọi từ component của mình để cho phép nó "nhớ" mọi thứ. Hãy lưu trữ giá trị hiện tại của `Square` trong state, và thay đổi nó khi `Square` được nhấp. -Import `useState` at the top of the file. Remove the `value` prop from the `Square` component. Instead, add a new line at the start of the `Square` that calls `useState`. Have it return a state variable called `value`: +Import `useState` ở đầu file. Xóa prop `value` khỏi component `Square`. Thay vào đó, thêm một dòng mới ở đầu `Square` gọi `useState`. Cho nó trả về một biến state có tên `value`: ```js {1,3,4} import { useState } from 'react'; @@ -747,9 +747,9 @@ function Square() { //... ``` -`value` stores the value and `setValue` is a function that can be used to change the value. The `null` passed to `useState` is used as the initial value for this state variable, so `value` here starts off equal to `null`. +`value` lưu trữ giá trị và `setValue` là một function có thể được sử dụng để thay đổi giá trị. `null` được truyền vào `useState` được sử dụng làm giá trị ban đầu cho biến state này, vì vậy `value` ở đây bắt đầu bằng `null`. -Since the `Square` component no longer accepts props anymore, you'll remove the `value` prop from all nine of the Square components created by the Board component: +Vì component `Square` không còn nhận props nữa, bạn sẽ xóa prop `value` khỏi tất cả chín component Square được tạo bởi component Board: ```js {6-8,11-13,16-18} // ... @@ -776,7 +776,7 @@ export default function Board() { } ``` -Now you'll change `Square` to display an "X" when clicked. Replace the `console.log("clicked!");` event handler with `setValue('X');`. Now your `Square` component looks like this: +Bây giờ bạn sẽ thay đổi `Square` để hiển thị một "X" khi được nhấp. Thay thế event handler `console.log("clicked!");` bằng `setValue('X');`. Bây giờ component `Square` của bạn trông như thế này: ```js {5} function Square() { @@ -797,13 +797,13 @@ function Square() { } ``` -By calling this `set` function from an `onClick` handler, you're telling React to re-render that `Square` whenever its `