diff --git a/src/content/learn/tutorial-tic-tac-toe.md b/src/content/learn/tutorial-tic-tac-toe.md
index c80c7f5fe..4f550d173 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 (
-
- {value}
-
- );
-}
-
-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 (
-
- jumpTo(move)}>{description}
-
- );
- });
-
- return (
-
- );
-}
-
-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 X ;
-}
-```
-
-```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:
-
-
-
-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 X ;
-}
-```
-
-The _browser_ section should be displaying a square with an X in it like this:
-
-
-
-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 (
+
+ {value}
+
+ );
+}
+
+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 (
+
+ jumpTo(move)}>{description}
+
+ );
+ });
+
+ return (
+
+ );
+}
+
+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 X ;
+}
+```
+
+```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:
+
+
+
+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 X ;
+}
+```
+
+Phần _browser_ nên hiển thị một ô vuông có chữ X bên trong như sau:
+
+
+
+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 X ;
-}
-```
-
-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 X ;
-}
-```
-
-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. `` is a *JSX element*. A JSX element is a combination of JavaScript code and HTML tags that describes what you'd like to display. `className="square"` is a button property or *prop* that tells CSS how to style the button. `X` is the text displayed inside of the button and ` ` 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 X ;
+}
+```
+
+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 X ;
+}
+```
+
+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. `` là một *phần tử JSX*. Một phần tử JSX là sự kết hợp giữa mã JavaScript và các thẻ HTML mô tả những gì bạn muốn hiển thị. `className="square"` là một thuộc tính button hoặc *prop* cho CSS biết cách tạo kiểu cho button. `X` là văn bản được hiển thị bên trong button và ` ` đó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 X X ;
-}
-```
-
-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 (
- <>
- X
- X
- >
- );
-}
-```
-
-Now you should see:
-
-
-
-Great! Now you just need to copy-paste a few times to add nine squares and...
-
-
-
-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 (
- <>
-
- 1
- 2
- 3
-
-
- 4
- 5
- 6
-
-
- 7
- 8
- 9
-
- >
- );
-}
-```
-
-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:
-
-
-
-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 (
- <>
-
- 1
- 2
- 3
-
-
- 4
- 5
- 6
-
-
- 7
- 8
- 9
-
- >
- );
-}
-```
-
-```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 (`1 `) from your `Board` component into a new `Square` component:
-
-```js {1-3}
-function Square() {
- return 1 ;
-}
-
-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:
-
-
-
-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 1 ;
-}
-```
-
-`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 value ;
-}
-```
-
-Oops, this is not what you wanted:
-
-
-
-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 {value} ;
-}
-```
-
-For now, you should see an empty board:
-
-
-
-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:
-
-
-
-Your updated code should look like this:
-
-
-
-```js src/App.js
-function Square({ value }) {
- return {value} ;
-}
-
-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 (
-
- {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.
-
-
-
-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 (
-
- {value}
-
- );
-}
-```
-
-By calling this `set` function from an `onClick` handler, you're telling React to re-render that `Square` whenever its `` is clicked. After the update, the `Square`'s `value` will be `'X'`, so you'll see the "X" on the game board. Click on any Square, and "X" should show up:
-
-
-
-Each Square has its own state: the `value` stored in each Square is completely independent of the others. When you call a `set` function in a component, React automatically updates the child components inside too.
-
-After you've made the above changes, your code will look like this:
-
-
-
-```js src/App.js
-import { useState } from 'react';
-
-function Square() {
- const [value, setValue] = useState(null);
-
- function handleClick() {
- setValue('X');
- }
-
- return (
-
- {value}
-
- );
-}
-
-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;
-}
-```
-
-
-
+
+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 X X ;
+}
+```
+
+Bạn sẽ gặp lỗi này:
+
+
+
+/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX Fragment `<>...>`?
+
+
+
+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() {
+ return (
+ <>
+ X
+ X
+ >
+ );
+}
+```
+
+Bây giờ bạn sẽ thấy:
+
+
+
+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à...
+
+
+
+Ồ 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.
+
+Trong file `App.js`, cập nhật component `Square` để trông như thế này:
+
+```js {3-19}
+export default function Square() {
+ return (
+ <>
+
+ 1
+ 2
+ 3
+
+
+ 4
+ 5
+ 6
+
+
+ 7
+ 8
+ 9
+
+ >
+ );
+}
+```
+
+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:
+
+
+
+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() {
+ //...
+}
+```
+
+Ở thời điểm này, code của bạn nên trông giống như thế này:
+
+
+
+```js
+export default function Board() {
+ return (
+ <>
+
+ 1
+ 2
+ 3
+
+
+ 4
+ 5
+ 6
+
+
+ 7
+ 8
+ 9
+
+ >
+ );
+}
+```
+
+```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... Đó 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.
+
+
+
+### Truyền dữ liệu qua props {/*passing-data-through-props*/}
+
+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.
+
+Đầu tiên, bạn sẽ sao chép dòng định nghĩa ô vuông đầu tiên của bạn (`1 `) từ component `Board` của bạn vào một component `Square` mới:
+
+```js {1-3}
+function Square() {
+ return 1 ;
+}
+
+export default function Board() {
+ // ...
+}
+```
+
+Sau đó bạn sẽ cập nhật component Board để render component `Square` đó bằng cú pháp JSX:
+
+```js {5-19}
+// ...
+export default function Board() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+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.
+
+Hãy xem:
+
+
+
+Ồ 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`).
+
+Cập nhật component `Square` để đọc prop `value` mà bạn sẽ truyền từ `Board`:
+
+```js {1}
+function Square({ value }) {
+ return 1 ;
+}
+```
+
+`function Square({ value })` cho biết component Square có thể được truyền một prop có tên `value`.
+
+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 }) {
+ return value ;
+}
+```
+
+Ồ, đây không phải là điều bạn muốn:
+
+
+
+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 }) {
+ return {value} ;
+}
+```
+
+Hiện tại, bạn sẽ thấy một bảng trống:
+
+
+
+Đ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() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+Bây giờ bạn sẽ lại thấy một lưới số:
+
+
+
+Code đã cập nhật của bạn nên trông như thế này:
+
+
+
+```js src/App.js
+function Square({ value }) {
+ return {value} ;
+}
+
+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;
+}
+```
+
+
+
+### Tạo một component tương tác {/*making-an-interactive-component*/}
+
+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 }) {
+ function handleClick() {
+ console.log('clicked!');
+ }
+
+ return (
+
+ {value}
+
+ );
+}
+```
+
+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.
+
+
+
+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).
+
+
+
+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 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` ở đầ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';
+
+function Square() {
+ const [value, setValue] = useState(null);
+
+ function handleClick() {
+ //...
+```
+
+`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`.
+
+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}
+// ...
+export default function Board() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+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() {
+ const [value, setValue] = useState(null);
+
+ function handleClick() {
+ setValue('X');
+ }
+
+ return (
+
+ {value}
+
+ );
+}
+```
+
+Bằng cách gọi function `set` này từ một handler `onClick`, bạn đang báo cho React biết để re-render `Square` đó bất cứ khi nào `` của nó được nhấp. Sau khi cập nhật, `value` của `Square` sẽ là `'X'`, vì vậy bạn sẽ thấy "X" trên bảng chơi. Nhấp vào bất kỳ Square nào, và "X" sẽ xuất hiện:
+
+
+
+Mỗi Square có state riêng của nó: `value` được lưu trữ trong mỗi Square hoàn toàn độc lập với các Square khác. Khi bạn gọi một function `set` trong một component, React tự động cập nhật các component con bên trong cũng vậy.
+
+Sau khi bạn đã thực hiện các thay đổi ở trên, code của bạn sẽ trông như thế này:
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square() {
+ const [value, setValue] = useState(null);
+
+ function handleClick() {
+ setValue('X');
+ }
+
+ return (
+
+ {value}
+
+ );
+}
+
+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:
-
-
-
-To inspect a particular component on the screen, use the button in the top left corner of React DevTools:
-
-
-
-
-
-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.
-
-### 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 {value} ;
-}
-```
-
-At this point you should see an empty tic-tac-toe board:
-
-
-
-And your code should look like this:
-
-
-
-```js src/App.js
-import { useState } from 'react';
-
-function Square({ value }) {
- return {value} ;
-}
-
-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 (
-
- {value}
-
- );
-}
-```
-
-Next, you'll add the `onSquareClick` function to the `Square` component's props:
-
-```js {1}
-function Square({ value, onSquareClick }) {
- return (
-
- {value}
-
- );
-}
-```
-
-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:
-
-
-
-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 (
-
- {value}
-
- );
-}
-
-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 `` element's `onClick` attribute has a special meaning to React because it is a built-in component. For custom components like Square, the naming is up to you. You could give any name to the `Square`'s `onSquareClick` prop or `Board`'s `handleClick` function, and the code would work the same. In React, it's conventional to use `onSomething` names for props which represent events and `handleSomething` for the function definitions which handle those events.
-
-
-
-### Why immutability is important {/*why-immutability-is-important*/}
-
-Note how in `handleClick`, you call `.slice()` to create a copy of the `squares` array instead of modifying the existing array. To explain why, we need to discuss immutability and why immutability is important to learn.
-
-There are generally two approaches to changing data. The first approach is to _mutate_ the data by directly changing the data's values. The second approach is to replace the data with a new copy which has the desired changes. Here is what it would look like if you mutated the `squares` array:
-
-```jsx
-const squares = [null, null, null, null, null, null, null, null, null];
-squares[0] = 'X';
-// Now `squares` is ["X", null, null, null, null, null, null, null, null];
-```
-
-And here is what it would look like if you changed data without mutating the `squares` array:
-
-```jsx
-const squares = [null, null, null, null, null, null, null, null, null];
-const nextSquares = ['X', null, null, null, null, null, null, null, null];
-// Now `squares` is unchanged, but `nextSquares` first element is 'X' rather than `null`
-```
-
-The result is the same but by not mutating (changing the underlying data) directly, you gain several benefits.
-
-Immutability makes complex features much easier to implement. Later in this tutorial, you will implement a "time travel" feature that lets you review the game's history and "jump back" to past moves. This functionality isn't specific to games--an ability to undo and redo certain actions is a common requirement for apps. Avoiding direct data mutation lets you keep previous versions of the data intact, and reuse them later.
-
-There is also another benefit of immutability. By default, all child components re-render automatically when the state of a parent component changes. This includes even the child components that weren't affected by the change. Although re-rendering is not by itself noticeable to the user (you shouldn't actively try to avoid it!), you might want to skip re-rendering a part of the tree that clearly wasn't affected by it for performance reasons. Immutability makes it very cheap for components to compare whether their data has changed or not. You can learn more about how React chooses when to re-render a component in [the `memo` API reference](/reference/react/memo).
-
-### Taking turns {/*taking-turns*/}
-
-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:
-
-
-
-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 (
-
- {value}
-
- );
-}
-
-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 (
-
- {value}
-
- );
-}
-
-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.
-
-### 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'],
- // ...
-]
-```
-
-### 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 (
-
- );
-}
-```
-
-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 (
-
- {value}
-
- );
-}
-
-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 (
-
- );
-}
-
-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 `
` are regular JavaScript objects; you can pass them around in your application. To render multiple items in React, you can use an array of React elements.
-
-You already have an array of `history` moves in state, so now you need to transform it to an array of React elements. In JavaScript, to transform one array into another, you can use the [array `map` method:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
-
-```jsx
-[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
-```
-
-You'll use `map` to transform your `history` of moves into React elements representing buttons on the screen, and display a list of buttons to "jump" to past moves. Let's `map` over the `history` in the Game component:
-
-```js {11-13,15-27,35}
-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 (
-
- jumpTo(move)}>{description}
-
- );
- });
-
- return (
-
- );
-}
-```
-
-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 (
-
- {value}
-
- );
-}
-
-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 (
-
- jumpTo(move)}>{description}
-
- );
- });
-
- return (
-
- );
-}
-
-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 ``. The button has an `onClick` handler which calls a function called `jumpTo` (that you haven't implemented yet).
-
-For now, you should see a list of the moves that occurred in the game and an error in the developer tools console. Let's discuss what the "key" error means.
-
-### Picking a key {/*picking-a-key*/}
-
-When you render a list, React stores some information about each rendered list item. When you update a list, React needs to determine what has changed. You could have added, removed, re-arranged, or updated the list's items.
-
-Imagine transitioning from
-
-```html
-Alexa: 7 tasks left
-Ben: 5 tasks left
-```
-
-to
-
-```html
-Ben: 9 tasks left
-Claudia: 8 tasks left
-Alexa: 5 tasks left
-```
-
-In addition to the updated counts, a human reading this would probably say that you swapped Alexa and Ben's ordering and inserted Claudia between Alexa and Ben. However, React is a computer program and does not know what you intended, so you need to specify a _key_ property for each list item to differentiate each list item from its siblings. If your data was from a database, Alexa, Ben, and Claudia's database IDs could be used as keys.
-
-```js {1}
-
- {user.name}: {user.taskCount} tasks left
-
-```
-
-When a list is re-rendered, React takes each list item's key and searches the previous list's items for a matching key. If the current list has a key that didn't exist before, React creates a component. If the current list is missing a key that existed in the previous list, React destroys the previous component. If two keys match, the corresponding component is moved.
-
-Keys tell React about the identity of each component, which allows React to maintain state between re-renders. If a component's key changes, the component will be destroyed and re-created with a new state.
-
-`key` is a special and reserved property in React. When an element is created, React extracts the `key` property and stores the key directly on the returned element. Even though `key` may look like it is passed as props, React automatically uses `key` to decide which components to update. There's no way for a component to ask what `key` its parent specified.
-
-**It's strongly recommended that you assign proper keys whenever you build dynamic lists.** If you don't have an appropriate key, you may want to consider restructuring your data so that you do.
-
-If no key is specified, React will report an error and use the array index as a key by default. Using the array index as a key is problematic when trying to re-order a list's items or inserting/removing list items. Explicitly passing `key={i}` silences the error but has the same problems as array indices and is not recommended in most cases.
-
-Keys do not need to be globally unique; they only need to be unique between components and their siblings.
-
-### Implementing time travel {/*implementing-time-travel*/}
-
-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 (
-
- jumpTo(move)}>{description}
-
- );
-});
-```
-
-
-
-```js src/App.js
-import { useState } from 'react';
-
-function Square({ value, onSquareClick }) {
- return (
-
- {value}
-
- );
-}
-
-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 (
-
- jumpTo(move)}>{description}
-
- );
- });
-
- return (
-
- );
-}
-
-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 (
-
- {value}
-
- );
-}
-
-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 (
-
- jumpTo(move)}>{description}
-
- );
- });
-
- return (
-
- );
-}
-
-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.
-
-### 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 (
-
- {value}
-
- );
-}
-
-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 (
-
- jumpTo(move)}>{description}
-
- );
- });
-
- return (
-
- );
-}
-
-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.
+
+React DevTools cho phép bạn kiểm tra props và state của các React components của bạn. Bạn có thể tìm thấy tab React DevTools ở cuối phần _browser_ trong CodeSandbox:
+
+
+
+Để kiểm tra một component cụ thể trên màn hình, sử dụng nút ở góc trên bên trái của React DevTools:
+
+
+
+
+
+Đối với phát triển local, React DevTools có sẵn dưới dạng extension trình duyệt [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/), và [Edge](https://microsoftedge.microsoft.com/addons/detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil). Cài đặt nó, và tab *Components* sẽ xuất hiện trong Developer Tools của trình duyệt của bạn cho các trang web sử dụng React.
+
+
+
+## Hoàn thiện trò chơi {/*completing-the-game*/}
+
+Đến thời điểm này, bạn đã có tất cả các khối xây dựng cơ bản cho trò chơi tic-tac-toe của mình. Để có một trò chơi hoàn chỉnh, bây giờ bạn cần luân phiên đặt "X" và "O" trên bảng, và bạn cần một cách để xác định người thắng.
+
+### Nâng state lên {/*lifting-state-up*/}
+
+Hiện tại, mỗi component `Square` duy trì một phần state của trò chơi. Để kiểm tra người thắng trong trò chơi tic-tac-toe, `Board` sẽ cần bằng cách nào đó biết state của mỗi trong số 9 component `Square`.
+
+Bạn sẽ tiếp cận điều đó như thế nào? Lúc đầu, bạn có thể đoán rằng `Board` cần "hỏi" mỗi `Square` về state của `Square` đó. Mặc dù cách tiếp cận này về mặt kỹ thuật có thể thực hiện được trong React, chúng tôi không khuyến khích vì code trở nên khó hiểu, dễ bị lỗi, và khó refactor. Thay vào đó, cách tiếp cận tốt nhất là lưu trữ state của trò chơi trong component cha `Board` thay vì trong mỗi `Square`. Component `Board` có thể cho mỗi `Square` biết cần hiển thị gì bằng cách truyền một prop, giống như bạn đã làm khi truyền một số cho mỗi Square.
+
+**Để thu thập dữ liệu từ nhiều component con, hoặc để có hai component con giao tiếp với nhau, hãy khai báo state dùng chung trong component cha của chúng. Component cha có thể truyền state đó xuống các component con thông qua props. Điều này giữ cho các component con đồng bộ với nhau và với component cha của chúng.**
+
+Nâng state lên component cha là điều phổ biến khi các React components được refactor.
+
+Hãy tận dụng cơ hội này để thử nó. Chỉnh sửa component `Board` để nó khai báo một biến state có tên `squares` mặc định là một mảng gồm 9 null tương ứng với 9 ô vuông:
+
+```js {3}
+// ...
+export default function Board() {
+ const [squares, setSquares] = useState(Array(9).fill(null));
+ return (
+ // ...
+ );
+}
+```
+
+`Array(9).fill(null)` tạo một mảng với chín phần tử và đặt mỗi phần tử thành `null`. Lời gọi `useState()` xung quanh nó khai báo một biến state `squares` ban đầu được đặt thành mảng đó. Mỗi mục trong mảng tương ứng với giá trị của một ô vuông. Khi bạn điền bảng sau này, mảng `squares` sẽ trông như thế này:
+
+```jsx
+['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
+```
+
+Bây giờ component `Board` của bạn cần truyền prop `value` xuống mỗi `Square` mà nó render:
+
+```js {6-8,11-13,16-18}
+export default function Board() {
+ const [squares, setSquares] = useState(Array(9).fill(null));
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+Tiếp theo, bạn sẽ chỉnh sửa component `Square` để nhận prop `value` từ component Board. Điều này sẽ yêu cầu xóa việc theo dõi state của `value` trong component Square và prop `onClick` của button:
+
+```js {1,2}
+function Square({value}) {
+ return {value} ;
+}
+```
+
+Ở thời điểm này bạn sẽ thấy một bảng chơi tic-tac-toe trống:
+
+
+
+Và code của bạn nên trông như thế này:
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({ value }) {
+ return {value} ;
+}
+
+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;
+}
+```
+
+
+
+Mỗi Square bây giờ sẽ nhận một prop `value` sẽ là `'X'`, `'O'`, hoặc `null` cho các ô vuông trống.
+
+Tiếp theo, bạn cần thay đổi điều gì xảy ra khi một `Square` được nhấp. Component `Board` bây giờ duy trì các ô vuông nào đã được điền. Bạn sẽ cần tạo một cách để `Square` cập nhật state của `Board`. Vì state là riêng tư đối với component định nghĩa nó, bạn không thể cập nhật state của `Board` trực tiếp từ `Square`.
+
+Thay vào đó, bạn sẽ truyền một function từ component `Board` xuống component `Square`, và bạn sẽ có `Square` gọi function đó khi một ô vuông được nhấp. Bạn sẽ bắt đầu với function mà component `Square` sẽ gọi khi nó được nhấp. Bạn sẽ gọi function đó là `onSquareClick`:
+
+```js {3}
+function Square({ value }) {
+ return (
+
+ {value}
+
+ );
+}
+```
+
+Tiếp theo, bạn sẽ thêm function `onSquareClick` vào props của component `Square`:
+
+```js {1}
+function Square({ value, onSquareClick }) {
+ return (
+
+ {value}
+
+ );
+}
+```
+
+Bây giờ bạn sẽ kết nối prop `onSquareClick` với một function trong component `Board` mà bạn sẽ đặt tên là `handleClick`. Để kết nối `onSquareClick` với `handleClick`, bạn sẽ truyền một function vào prop `onSquareClick` của component `Square` đầu tiên:
+
+```js {7}
+export default function Board() {
+ const [squares, setSquares] = useState(Array(9).fill(null));
+
+ return (
+ <>
+
+
+ //...
+ );
+}
+```
+
+Cuối cùng, bạn sẽ định nghĩa function `handleClick` bên trong component Board để cập nhật mảng `squares` chứa state của bảng chơi:
+
+```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 (
+ // ...
+ )
+}
+```
+
+Function `handleClick` tạo một bản sao của mảng `squares` (`nextSquares`) bằng phương thức Array `slice()` của JavaScript. Sau đó, `handleClick` cập nhật mảng `nextSquares` để thêm `X` vào ô vuông đầu tiên (index `[0]`).
+
+Gọi function `setSquares` cho React biết state của component đã thay đổi. Điều này sẽ kích hoạt re-render của các components sử dụng state `squares` (`Board`) cũng như các component con của nó (các component `Square` tạo nên bảng chơi).
+
+
+
+JavaScript hỗ trợ [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) có nghĩa là một function bên trong (ví dụ: `handleClick`) có quyền truy cập vào các biến và functions được định nghĩa trong một function bên ngoài (ví dụ: `Board`). Function `handleClick` có thể đọc state `squares` và gọi phương thức `setSquares` vì cả hai đều được định nghĩa bên trong function `Board`.
+
+
+
+Bây giờ bạn có thể thêm X vào bảng... nhưng chỉ vào ô vuông trên bên trái. Function `handleClick` của bạn được hardcode để cập nhật index cho ô vuông trên bên trái (`0`). Hãy cập nhật `handleClick` để có thể cập nhật bất kỳ ô vuông nào. Thêm một đối số `i` vào function `handleClick` nhận index của ô vuông cần cập nhật:
+
+```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 (
+ // ...
+ )
+}
+```
+
+Tiếp theo, bạn sẽ cần truyền `i` đó vào `handleClick`. Bạn có thể thử đặt prop `onSquareClick` của square thành `handleClick(0)` trực tiếp trong JSX như thế này, nhưng nó sẽ không hoạt động:
+
+```jsx
+
+```
+
+Đây là lý do tại sao điều này không hoạt động. Lời gọi `handleClick(0)` sẽ là một phần của việc render component board. Vì `handleClick(0)` thay đổi state của component board bằng cách gọi `setSquares`, toàn bộ component board của bạn sẽ được re-render lại. Nhưng điều này lại chạy `handleClick(0)` một lần nữa, dẫn đến một vòng lặp vô hạn:
+
+
+
+Too many re-renders. React limits the number of renders to prevent an infinite loop.
+
+
+
+Tại sao vấn đề này không xảy ra trước đó?
+
+Khi bạn truyền `onSquareClick={handleClick}`, bạn đang truyền function `handleClick` xuống như một prop. Bạn không gọi nó! Nhưng bây giờ bạn đang *gọi* function đó ngay lập tức--chú ý dấu ngoặc đơn trong `handleClick(0)`--và đó là lý do tại sao nó chạy quá sớm. Bạn không *muốn* gọi `handleClick` cho đến khi người dùng nhấp!
+
+Bạn có thể sửa lỗi này bằng cách tạo một function như `handleFirstSquareClick` gọi `handleClick(0)`, một function như `handleSecondSquareClick` gọi `handleClick(1)`, và cứ như vậy. Bạn sẽ truyền (thay vì gọi) các functions này xuống như props như `onSquareClick={handleFirstSquareClick}`. Điều này sẽ giải quyết vòng lặp vô hạn.
+
+Tuy nhiên, định nghĩa chín functions khác nhau và đặt tên cho mỗi function là quá dài dòng. Thay vào đó, hãy làm như sau:
+
+```js {6}
+export default function Board() {
+ // ...
+ return (
+ <>
+
+
handleClick(0)} />
+ // ...
+ );
+}
+```
+
+Chú ý cú pháp mới `() =>`. Ở đây, `() => handleClick(0)` là một *arrow function,* đây là cách ngắn gọn hơn để định nghĩa functions. Khi ô vuông được nhấp, code sau dấu "mũi tên" `=>` sẽ chạy, gọi `handleClick(0)`.
+
+Bây giờ bạn cần cập nhật tám ô vuông còn lại để gọi `handleClick` từ các arrow functions bạn truyền. Đảm bảo rằng đối số cho mỗi lần gọi `handleClick` tương ứng với index của ô vuông đúng:
+
+```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)} />
+
+ >
+ );
+};
+```
+
+Bây giờ bạn có thể lại thêm X vào bất kỳ ô vuông nào trên bảng bằng cách nhấp vào chúng:
+
+
+
+Nhưng lần này tất cả việc quản lý state được xử lý bởi component `Board`!
+
+Đây là cách code của bạn nên trông như thế:
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({ value, onSquareClick }) {
+ return (
+
+ {value}
+
+ );
+}
+
+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;
+}
+```
+
+
+
+Bây giờ việc xử lý state của bạn đã ở trong component `Board`, component cha `Board` truyền props xuống các component con `Square` để chúng có thể được hiển thị đúng. Khi nhấp vào một `Square`, component con `Square` bây giờ yêu cầu component cha `Board` cập nhật state của bảng. Khi state của `Board` thay đổi, cả component `Board` và mọi component con `Square` đều tự động re-render. Giữ state của tất cả các ô vuông trong component `Board` sẽ cho phép nó xác định người thắng trong tương lai.
+
+Hãy tóm tắt lại những gì xảy ra khi người dùng nhấp vào ô vuông trên bên trái trên bảng của bạn để thêm một `X` vào đó:
+
+1. Nhấp vào ô vuông trên bên trái chạy function mà `button` nhận được như prop `onClick` của nó từ `Square`. Component `Square` nhận được function đó như prop `onSquareClick` của nó từ `Board`. Component `Board` đã định nghĩa function đó trực tiếp trong JSX. Nó gọi `handleClick` với đối số là `0`.
+1. `handleClick` sử dụng đối số (`0`) để cập nhật phần tử đầu tiên của mảng `squares` từ `null` thành `X`.
+1. State `squares` của component `Board` đã được cập nhật, vì vậy `Board` và tất cả các component con của nó re-render. Điều này làm cho prop `value` của component `Square` với index `0` thay đổi từ `null` thành `X`.
+
+Cuối cùng người dùng thấy rằng ô vuông trên bên trái đã thay đổi từ trống thành có `X` sau khi nhấp vào nó.
+
+
+
+Thuộc tính `onClick` của phần tử DOM `` có ý nghĩa đặc biệt đối với React vì nó là một component tích hợp sẵn. Đối với các component tùy chỉnh như Square, việc đặt tên là tùy bạn. Bạn có thể đặt bất kỳ tên nào cho prop `onSquareClick` của `Square` hoặc function `handleClick` của `Board`, và code sẽ hoạt động giống nhau. Trong React, quy ước là sử dụng tên `onSomething` cho props đại diện cho events và `handleSomething` cho các định nghĩa function xử lý các events đó.
+
+
+
+### Tại sao tính bất biến lại quan trọng {/*why-immutability-is-important*/}
+
+Chú ý cách trong `handleClick`, bạn gọi `.slice()` để tạo một bản sao của mảng `squares` thay vì sửa đổi mảng hiện có. Để giải thích tại sao, chúng ta cần thảo luận về tính bất biến và tại sao tính bất biến lại quan trọng để học.
+
+Nhìn chung có hai cách tiếp cận để thay đổi dữ liệu. Cách tiếp cận đầu tiên là _mutate_ (đột biến) dữ liệu bằng cách trực tiếp thay đổi các giá trị của dữ liệu. Cách tiếp cận thứ hai là thay thế dữ liệu bằng một bản sao mới có các thay đổi mong muốn. Đây là cách nó sẽ trông như thế nào nếu bạn mutate mảng `squares`:
+
+```jsx
+const squares = [null, null, null, null, null, null, null, null, null];
+squares[0] = 'X';
+// Bây giờ `squares` là ["X", null, null, null, null, null, null, null, null];
+```
+
+Và đây là cách nó sẽ trông như thế nào nếu bạn thay đổi dữ liệu mà không mutate mảng `squares`:
+
+```jsx
+const squares = [null, null, null, null, null, null, null, null, null];
+const nextSquares = ['X', null, null, null, null, null, null, null, null];
+// Bây giờ `squares` không thay đổi, nhưng phần tử đầu tiên của `nextSquares` là 'X' thay vì `null`
+```
+
+Kết quả là giống nhau nhưng bằng cách không mutate (thay đổi dữ liệu cơ bản) trực tiếp, bạn có được một số lợi ích.
+
+Tính bất biến làm cho các tính năng phức tạp dễ triển khai hơn nhiều. Sau này trong hướng dẫn này, bạn sẽ triển khai tính năng "du hành thời gian" cho phép bạn xem lại lịch sử trò chơi và "nhảy ngược" về các nước đi trước đó. Chức năng này không chỉ dành riêng cho trò chơi--khả năng undo và redo các hành động nhất định là yêu cầu phổ biến cho các ứng dụng. Tránh đột biến dữ liệu trực tiếp cho phép bạn giữ nguyên các phiên bản trước đó của dữ liệu và sử dụng lại chúng sau này.
+
+Cũng có một lợi ích khác của tính bất biến. Theo mặc định, tất cả các component con tự động re-render khi state của component cha thay đổi. Điều này bao gồm cả các component con không bị ảnh hưởng bởi thay đổi. Mặc dù re-rendering tự nó không đáng chú ý đối với người dùng (bạn không nên cố gắng tránh nó!), bạn có thể muốn bỏ qua re-rendering một phần của cây rõ ràng không bị ảnh hưởng bởi nó vì lý do hiệu suất. Tính bất biến làm cho việc so sánh xem dữ liệu của components đã thay đổi hay chưa trở nên rất rẻ. Bạn có thể tìm hiểu thêm về cách React chọn khi nào để re-render một component trong [tham chiếu API `memo`](/reference/react/memo).
+
+### Luân phiên lượt chơi {/*taking-turns*/}
+
+Bây giờ đã đến lúc sửa một lỗi lớn trong trò chơi tic-tac-toe này: các "O" không thể được đánh dấu trên bảng.
+
+Bạn sẽ đặt nước đi đầu tiên là "X" theo mặc định. Hãy theo dõi điều này bằng cách thêm một phần state khác vào component Board:
+
+```js {2}
+function Board() {
+ const [xIsNext, setXIsNext] = useState(true);
+ const [squares, setSquares] = useState(Array(9).fill(null));
+
+ // ...
+}
+```
+
+Mỗi khi người chơi di chuyển, `xIsNext` (một boolean) sẽ được đảo ngược để xác định người chơi nào đi tiếp theo và state của trò chơi sẽ được lưu. Bạn sẽ cập nhật function `handleClick` của `Board` để đảo ngược giá trị của `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 (
+ //...
+ );
+}
+```
+
+Bây giờ, khi bạn nhấp vào các ô vuông khác nhau, chúng sẽ luân phiên giữa `X` và `O`, như chúng nên!
+
+Nhưng đợi đã, có một vấn đề. Hãy thử nhấp vào cùng một ô vuông nhiều lần:
+
+
+
+`X` bị ghi đè bởi một `O`! Mặc dù điều này sẽ thêm một biến thể rất thú vị cho trò chơi, chúng ta sẽ tuân theo các quy tắc gốc cho bây giờ.
+
+Khi bạn đánh dấu một ô vuông bằng `X` hoặc `O`, bạn không kiểm tra trước xem ô vuông đó đã có giá trị `X` hoặc `O` chưa. Bạn có thể sửa lỗi này bằng cách *return sớm*. Bạn sẽ kiểm tra xem ô vuông đã có `X` hoặc `O` chưa. Nếu ô vuông đã được điền, bạn sẽ `return` trong function `handleClick` sớm--trước khi nó cố gắng cập nhật state của bảng.
+
+```js {2,3,4}
+function handleClick(i) {
+ if (squares[i]) {
+ return;
+ }
+ const nextSquares = squares.slice();
+ //...
+}
+```
+
+Bây giờ bạn chỉ có thể thêm `X` hoặc `O` vào các ô vuông trống! Đây là cách code của bạn nên trông như ở thời điểm này:
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({value, onSquareClick}) {
+ return (
+
+ {value}
+
+ );
+}
+
+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;
+}
+```
+
+
+
+### Khai báo người thắng {/*declaring-a-winner*/}
+
+Bây giờ người chơi có thể luân phiên, bạn sẽ muốn hiển thị khi trò chơi đã thắng và không còn nước đi nào để thực hiện. Để làm điều này, bạn sẽ thêm một helper function có tên `calculateWinner` nhận một mảng gồm 9 ô vuông, kiểm tra người thắng và trả về `'X'`, `'O'`, hoặc `null` tùy theo trường hợp. Đừng lo lắng quá nhiều về function `calculateWinner`; nó không đặc biệt dành cho 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;
+}
+```
+
+
+
+Không quan trọng việc bạn định nghĩa `calculateWinner` trước hay sau `Board`. Hãy đặt nó ở cuối để bạn không phải cuộn qua nó mỗi lần chỉnh sửa components của mình.
+
+
+
+Bạn sẽ gọi `calculateWinner(squares)` trong function `handleClick` của component `Board` để kiểm tra xem người chơi đã thắng chưa. Bạn có thể thực hiện kiểm tra này cùng lúc với việc kiểm tra xem người dùng đã nhấp vào một ô vuông đã có `X` hoặc `O` chưa. Chúng ta muốn return sớm trong cả hai trường hợp:
+
+```js {2}
+function handleClick(i) {
+ if (squares[i] || calculateWinner(squares)) {
+ return;
+ }
+ const nextSquares = squares.slice();
+ //...
+}
+```
+
+Để cho người chơi biết khi trò chơi kết thúc, bạn có thể hiển thị văn bản như "Winner: X" hoặc "Winner: O". Để làm điều đó, bạn sẽ thêm một phần `status` vào component `Board`. Status sẽ hiển thị người thắng nếu trò chơi đã kết thúc và nếu trò chơi đang diễn ra, bạn sẽ hiển thị lượt chơi tiếp theo của người chơi nào:
+
+```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}
+
+ // ...
+ )
+}
+```
+
+Chúc mừng! Bây giờ bạn đã có một trò chơi tic-tac-toe hoạt động. Và bạn cũng vừa học được những điều cơ bản của React. Vì vậy _bạn_ là người thắng thực sự ở đây. Đây là cách code của bạn nên trông như thế:
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({value, onSquareClick}) {
+ return (
+
+ {value}
+
+ );
+}
+
+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;
+}
+```
+
+
+
+## Thêm tính năng du hành thời gian {/*adding-time-travel*/}
+
+Như một bài tập cuối cùng, hãy làm cho việc "quay ngược thời gian" về các nước đi trước đó trong trò chơi trở nên có thể.
+
+### Lưu trữ lịch sử các nước đi {/*storing-a-history-of-moves*/}
+
+Nếu bạn mutate mảng `squares`, việc triển khai tính năng du hành thời gian sẽ rất khó khăn.
+
+Tuy nhiên, bạn đã sử dụng `slice()` để tạo một bản sao mới của mảng `squares` sau mỗi nước đi, và xử lý nó như bất biến. Điều này sẽ cho phép bạn lưu trữ mọi phiên bản trước đó của mảng `squares`, và điều hướng giữa các lượt đã xảy ra.
+
+Bạn sẽ lưu trữ các mảng `squares` trước đó trong một mảng khác có tên `history`, mà bạn sẽ lưu trữ như một biến state mới. Mảng `history` đại diện cho tất cả các trạng thái bảng, từ nước đi đầu tiên đến nước đi cuối cùng, và có hình dạng như thế này:
+
+```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'],
+ // ...
+]
+```
+
+### Nâng state lên, một lần nữa {/*lifting-state-up-again*/}
+
+Bây giờ bạn sẽ viết một component cấp cao mới có tên `Game` để hiển thị danh sách các nước đi trước đó. Đó là nơi bạn sẽ đặt state `history` chứa toàn bộ lịch sử trò chơi.
+
+Đặt state `history` vào component `Game` sẽ cho phép bạn xóa state `squares` khỏi component con `Board` của nó. Giống như bạn đã "nâng state lên" từ component `Square` vào component `Board`, bây giờ bạn sẽ nâng nó lên từ `Board` vào component cấp cao `Game`. Điều này cho component `Game` toàn quyền kiểm soát dữ liệu của `Board` và cho phép nó hướng dẫn `Board` render các lượt trước đó từ `history`.
+
+Đầu tiên, thêm một component `Game` với `export default`. Cho nó render component `Board` và một số markup:
+
+```js {1,5-16}
+function Board() {
+ // ...
+}
+
+export default function Game() {
+ return (
+
+ );
+}
+```
+
+Lưu ý rằng bạn đang xóa các từ khóa `export default` trước khai báo `function Board() {` và thêm chúng trước khai báo `function Game() {`. Điều này cho file `index.js` của bạn biết sử dụng component `Game` như component cấp cao thay vì component `Board` của bạn. Các `div`s bổ sung được trả về bởi component `Game` đang tạo chỗ cho thông tin trò chơi mà bạn sẽ thêm vào bảng sau này.
+
+Thêm một số state vào component `Game` để theo dõi người chơi nào đi tiếp theo và lịch sử các nước đi:
+
+```js {2-3}
+export default function Game() {
+ const [xIsNext, setXIsNext] = useState(true);
+ const [history, setHistory] = useState([Array(9).fill(null)]);
+ // ...
+```
+
+Chú ý cách `[Array(9).fill(null)]` là một mảng với một mục duy nhất, bản thân nó là một mảng gồm 9 `null`.
+
+Để render các ô vuông cho nước đi hiện tại, bạn sẽ muốn đọc mảng squares cuối cùng từ `history`. Bạn không cần `useState` cho việc này--bạn đã có đủ thông tin để tính toán nó trong quá trình render:
+
+```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];
+ // ...
+```
+
+Tiếp theo, tạo một function `handlePlay` bên trong component `Game` sẽ được gọi bởi component `Board` để cập nhật trò chơi. Truyền `xIsNext`, `currentSquares` và `handlePlay` như props cho component `Board`:
+
+```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 (
+
+
+
+ //...
+ )
+}
+```
+
+Hãy làm cho component `Board` hoàn toàn được điều khiển bởi các props mà nó nhận được. Thay đổi component `Board` để nhận ba props: `xIsNext`, `squares`, và một function `onPlay` mới mà `Board` có thể gọi với mảng squares đã cập nhật khi người chơi thực hiện một nước đi. Tiếp theo, xóa hai dòng đầu tiên của function `Board` gọi `useState`:
+
+```js {1}
+function Board({ xIsNext, squares, onPlay }) {
+ function handleClick(i) {
+ //...
+ }
+ // ...
+}
+```
+
+Bây giờ thay thế các lời gọi `setSquares` và `setXIsNext` trong `handleClick` trong component `Board` bằng một lời gọi duy nhất đến function `onPlay` mới của bạn để component `Game` có thể cập nhật `Board` khi người dùng nhấp vào một ô vuông:
+
+```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);
+ }
+ //...
+}
+```
+
+Component `Board` hoàn toàn được điều khiển bởi các props được truyền cho nó bởi component `Game`. Bạn cần triển khai function `handlePlay` trong component `Game` để trò chơi hoạt động lại.
+
+`handlePlay` nên làm gì khi được gọi? Hãy nhớ rằng Board trước đây gọi `setSquares` với một mảng đã cập nhật; bây giờ nó truyền mảng `squares` đã cập nhật cho `onPlay`.
+
+Function `handlePlay` cần cập nhật state của `Game` để kích hoạt re-render, nhưng bạn không còn có function `setSquares` để gọi nữa--bây giờ bạn đang sử dụng biến state `history` để lưu trữ thông tin này. Bạn sẽ muốn cập nhật `history` bằng cách thêm mảng `squares` đã cập nhật như một mục lịch sử mới. Bạn cũng muốn đảo ngược `xIsNext`, giống như Board đã từng làm:
+
+```js {4-5}
+export default function Game() {
+ //...
+ function handlePlay(nextSquares) {
+ setHistory([...history, nextSquares]);
+ setXIsNext(!xIsNext);
+ }
+ //...
+}
+```
+
+Ở đây, `[...history, nextSquares]` tạo một mảng mới chứa tất cả các mục trong `history`, theo sau bởi `nextSquares`. (Bạn có thể đọc `...history` [*spread syntax*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) như "liệt kê tất cả các mục trong `history`".)
+
+Ví dụ, nếu `history` là `[[null,null,null], ["X",null,null]]` và `nextSquares` là `["X",null,"O"]`, thì mảng `[...history, nextSquares]` mới sẽ là `[[null,null,null], ["X",null,null], ["X",null,"O"]]`.
+
+Ở thời điểm này, bạn đã di chuyển state để sống trong component `Game`, và UI sẽ hoạt động đầy đủ, giống như trước khi refactor. Đây là cách code của bạn nên trông như ở thời điểm này:
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({ value, onSquareClick }) {
+ return (
+
+ {value}
+
+ );
+}
+
+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 (
+
+ );
+}
+
+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;
+}
+```
+
+
+
+### Hiển thị các nước đi trước đó {/*showing-the-past-moves*/}
+
+Vì bạn đang ghi lại lịch sử trò chơi tic-tac-toe, bạn có thể hiển thị danh sách các nước đi trước đó cho người chơi.
+
+Các phần tử React như `
` là các đối tượng JavaScript thông thường; bạn có thể truyền chúng xung quanh trong ứng dụng của mình. Để render nhiều mục trong React, bạn có thể sử dụng một mảng các phần tử React.
+
+Bạn đã có một mảng các nước đi `history` trong state, vì vậy bây giờ bạn cần biến đổi nó thành một mảng các phần tử React. Trong JavaScript, để biến đổi một mảng thành mảng khác, bạn có thể sử dụng [phương thức `map` của mảng:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
+
+```jsx
+[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
+```
+
+Bạn sẽ sử dụng `map` để biến đổi `history` các nước đi của bạn thành các phần tử React đại diện cho các button trên màn hình, và hiển thị một danh sách các button để "nhảy" đến các nước đi trước đó. Hãy `map` qua `history` trong component Game:
+
+```js {11-13,15-27,35}
+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 (
+
+ jumpTo(move)}>{description}
+
+ );
+ });
+
+ return (
+
+ );
+}
+```
+
+Bạn có thể xem code của bạn nên trông như thế nào bên dưới. Lưu ý rằng bạn sẽ thấy một lỗi trong console của developer tools nói rằng:
+
+
+Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Game`.
+
+
+Bạn sẽ sửa lỗi này trong phần tiếp theo.
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({ value, onSquareClick }) {
+ return (
+
+ {value}
+
+ );
+}
+
+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 (
+
+ jumpTo(move)}>{description}
+
+ );
+ });
+
+ return (
+
+ );
+}
+
+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;
+}
+```
+
+
+
+Khi bạn lặp qua mảng `history` bên trong function bạn đã truyền cho `map`, đối số `squares` đi qua từng phần tử của `history`, và đối số `move` đi qua từng chỉ số mảng: `0`, `1`, `2`, …. (Trong hầu hết các trường hợp, bạn sẽ cần các phần tử mảng thực tế, nhưng để render danh sách các nước đi, bạn sẽ chỉ cần các chỉ số.)
+
+Đối với mỗi nước đi trong lịch sử trò chơi tic-tac-toe, bạn tạo một mục danh sách `` chứa một button ``. Button có một handler `onClick` gọi một function có tên `jumpTo` (mà bạn chưa triển khai).
+
+Bây giờ, bạn sẽ thấy một danh sách các nước đi đã xảy ra trong trò chơi và một lỗi trong console của developer tools. Hãy thảo luận về ý nghĩa của lỗi "key".
+
+### Chọn một key {/*picking-a-key*/}
+
+Khi bạn render một danh sách, React lưu trữ một số thông tin về mỗi mục danh sách đã render. Khi bạn cập nhật một danh sách, React cần xác định những gì đã thay đổi. Bạn có thể đã thêm, xóa, sắp xếp lại, hoặc cập nhật các mục của danh sách.
+
+Hãy tưởng tượng chuyển đổi từ
+
+```html
+Alexa: 7 tasks left
+Ben: 5 tasks left
+```
+
+sang
+
+```html
+Ben: 9 tasks left
+Claudia: 8 tasks left
+Alexa: 5 tasks left
+```
+
+Ngoài các số đếm đã cập nhật, một người đọc điều này có thể sẽ nói rằng bạn đã đổi thứ tự của Alexa và Ben và chèn Claudia vào giữa Alexa và Ben. Tuy nhiên, React là một chương trình máy tính và không biết bạn muốn làm gì, vì vậy bạn cần chỉ định một thuộc tính _key_ cho mỗi mục danh sách để phân biệt mỗi mục danh sách với các mục anh em của nó. Nếu dữ liệu của bạn đến từ cơ sở dữ liệu, các ID cơ sở dữ liệu của Alexa, Ben, và Claudia có thể được sử dụng làm keys.
+
+```js {1}
+
+ {user.name}: {user.taskCount} tasks left
+
+```
+
+Khi một danh sách được re-render, React lấy key của mỗi mục danh sách và tìm kiếm các mục của danh sách trước đó để tìm một key khớp. Nếu danh sách hiện tại có một key không tồn tại trước đó, React tạo một component. Nếu danh sách hiện tại thiếu một key đã tồn tại trong danh sách trước đó, React hủy component trước đó. Nếu hai keys khớp, component tương ứng được di chuyển.
+
+Keys cho React biết về danh tính của mỗi component, điều này cho phép React duy trì state giữa các lần re-render. Nếu key của một component thay đổi, component sẽ bị hủy và được tạo lại với một state mới.
+
+`key` là một thuộc tính đặc biệt và được bảo lưu trong React. Khi một phần tử được tạo, React trích xuất thuộc tính `key` và lưu key trực tiếp trên phần tử được trả về. Mặc dù `key` có thể trông như nó được truyền như props, React tự động sử dụng `key` để quyết định component nào cần cập nhật. Không có cách nào để một component hỏi `key` mà component cha của nó đã chỉ định.
+
+**Được khuyến nghị mạnh mẽ rằng bạn gán keys phù hợp bất cứ khi nào bạn xây dựng danh sách động.** Nếu bạn không có một key phù hợp, bạn có thể muốn xem xét cấu trúc lại dữ liệu của mình để có.
+
+Nếu không có key nào được chỉ định, React sẽ báo lỗi và sử dụng chỉ số mảng làm key theo mặc định. Sử dụng chỉ số mảng làm key có vấn đề khi cố gắng sắp xếp lại các mục của danh sách hoặc chèn/xóa các mục danh sách. Truyền rõ ràng `key={i}` làm im lặng lỗi nhưng có cùng vấn đề như chỉ số mảng và không được khuyến nghị trong hầu hết các trường hợp.
+
+Keys không cần phải duy nhất toàn cục; chúng chỉ cần duy nhất giữa các component và các anh em của chúng.
+
+### Triển khai tính năng du hành thời gian {/*implementing-time-travel*/}
+
+Trong lịch sử trò chơi tic-tac-toe, mỗi nước đi trước đó có một ID duy nhất liên quan đến nó: đó là số thứ tự tuần tự của nước đi. Các nước đi sẽ không bao giờ được sắp xếp lại, xóa, hoặc chèn vào giữa, vì vậy an toàn khi sử dụng chỉ số nước đi làm key.
+
+Trong function `Game`, bạn có thể thêm key như ``, và nếu bạn tải lại trò chơi đã render, lỗi "key" của React sẽ biến mất:
+
+```js {4}
+const moves = history.map((squares, move) => {
+ //...
+ return (
+
+ jumpTo(move)}>{description}
+
+ );
+});
+```
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({ value, onSquareClick }) {
+ return (
+
+ {value}
+
+ );
+}
+
+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 (
+
+ jumpTo(move)}>{description}
+
+ );
+ });
+
+ return (
+
+ );
+}
+
+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;
+}
+```
+
+
+
+Trước khi bạn có thể triển khai `jumpTo`, bạn cần component `Game` theo dõi bước nào người dùng đang xem. Để làm điều này, định nghĩa một biến state mới có tên `currentMove`, mặc định là `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];
+ //...
+}
+```
+
+Tiếp theo, cập nhật function `jumpTo` bên trong `Game` để cập nhật `currentMove` đó. Bạn cũng sẽ đặt `xIsNext` thành `true` nếu số mà bạn đang thay đổi `currentMove` thành là số chẵn.
+
+```js {4-5}
+export default function Game() {
+ // ...
+ function jumpTo(nextMove) {
+ setCurrentMove(nextMove);
+ setXIsNext(nextMove % 2 === 0);
+ }
+ //...
+}
+```
+
+Bây giờ bạn sẽ thực hiện hai thay đổi cho function `handlePlay` của `Game` được gọi khi bạn nhấp vào một ô vuông.
+
+- Nếu bạn "quay ngược thời gian" và sau đó thực hiện một nước đi mới từ điểm đó, bạn chỉ muốn giữ lại lịch sử đến điểm đó. Thay vì thêm `nextSquares` sau tất cả các mục (cú pháp `...` spread) trong `history`, bạn sẽ thêm nó sau tất cả các mục trong `history.slice(0, currentMove + 1)` để bạn chỉ giữ lại phần đó của lịch sử cũ.
+- Mỗi khi một nước đi được thực hiện, bạn cần cập nhật `currentMove` để trỏ đến mục lịch sử mới nhất.
+
+```js {2-4}
+function handlePlay(nextSquares) {
+ const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
+ setHistory(nextHistory);
+ setCurrentMove(nextHistory.length - 1);
+ setXIsNext(!xIsNext);
+}
+```
+
+Cuối cùng, bạn sẽ sửa đổi component `Game` để render nước đi hiện tại được chọn, thay vì luôn render nước đi cuối cùng:
+
+```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];
+
+ // ...
+}
+```
+
+Nếu bạn nhấp vào bất kỳ bước nào trong lịch sử trò chơi, bảng chơi tic-tac-toe sẽ ngay lập tức cập nhật để hiển thị bảng chơi trông như thế nào sau khi bước đó xảy ra.
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({value, onSquareClick}) {
+ return (
+
+ {value}
+
+ );
+}
+
+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 (
+
+ jumpTo(move)}>{description}
+
+ );
+ });
+
+ return (
+
+ );
+}
+
+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;
+}
+```
+
+
+
+### Dọn dẹp cuối cùng {/*final-cleanup*/}
+
+Nếu bạn nhìn code rất kỹ, bạn có thể nhận thấy rằng `xIsNext === true` khi `currentMove` là số chẵn và `xIsNext === false` khi `currentMove` là số lẻ. Nói cách khác, nếu bạn biết giá trị của `currentMove`, thì bạn luôn có thể tính ra `xIsNext` nên là gì.
+
+Không có lý do gì để bạn lưu trữ cả hai trong state. Trên thực tế, luôn cố gắng tránh state dư thừa. Đơn giản hóa những gì bạn lưu trữ trong state giảm lỗi và làm cho code của bạn dễ hiểu hơn. Thay đổi `Game` để nó không lưu trữ `xIsNext` như một biến state riêng biệt và thay vào đó tính toán nó dựa trên `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);
+ }
+ // ...
+}
+```
+
+Bạn không còn cần khai báo state `xIsNext` hoặc các lời gọi đến `setXIsNext`. Bây giờ, không có khả năng `xIsNext` bị mất đồng bộ với `currentMove`, ngay cả khi bạn mắc lỗi khi code các components.
+
+### Tổng kết {/*wrapping-up*/}
+
+Chúc mừng! Bạn đã tạo một trò chơi tic-tac-toe mà:
+
+- Cho phép bạn chơi tic-tac-toe,
+- Cho biết khi nào một người chơi đã thắng trò chơi,
+- Lưu trữ lịch sử trò chơi khi trò chơi diễn ra,
+- Cho phép người chơi xem lại lịch sử trò chơi và xem các phiên bản trước đó của bảng chơi.
+
+Làm tốt lắm! Chúng tôi hy vọng bây giờ bạn cảm thấy như bạn đã nắm được cách React hoạt động.
+
+Xem kết quả cuối cùng ở đây:
+
+
+
+```js src/App.js
+import { useState } from 'react';
+
+function Square({ value, onSquareClick }) {
+ return (
+
+ {value}
+
+ );
+}
+
+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 (
+
+ jumpTo(move)}>{description}
+
+ );
+ });
+
+ return (
+
+ );
+}
+
+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 bạn có thời gian rảnh hoặc muốn luyện tập các kỹ năng React mới của mình, đây là một số ý tưởng cải tiến mà bạn có thể thực hiện cho trò chơi tic-tac-toe, được liệt kê theo thứ tự độ khó tăng dần:
+
+1. Chỉ cho nước đi hiện tại, hiển thị "Bạn đang ở nước đi #..." thay vì một button.
+1. Viết lại `Board` để sử dụng hai vòng lặp để tạo các ô vuông thay vì hardcode chúng.
+1. Thêm một toggle button cho phép bạn sắp xếp các nước đi theo thứ tự tăng dần hoặc giảm dần.
+1. Khi ai đó thắng, làm nổi bật ba ô vuông gây ra chiến thắng (và khi không ai thắng, hiển thị một thông báo về kết quả là hòa).
+1. Hiển thị vị trí cho mỗi nước đi theo định dạng (row, col) trong danh sách lịch sử nước đi.
+
+Trong suốt hướng dẫn này, bạn đã tiếp xúc với các khái niệm React bao gồm elements, components, props, và state. Bây giờ bạn đã thấy cách các khái niệm này hoạt động khi xây dựng một trò chơi, hãy xem [Tư duy trong React](/learn/thinking-in-react) để xem cách các khái niệm React tương tự hoạt động khi xây dựng UI của một ứng dụng.