Skip to main content

Bài 02-Tic-Tac-Toe

Bạn sẽ xây dựng một trò chơi tic-tac-toe nhỏ trong hướng dẫn này. Hướng dẫn này không giả định bất kỳ kiến ​​thức React nào hiệ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à hiểu đầy đủ về nó sẽ giúp bạn hiểu sâu hơn về React.

Ghi chú

Hướng dẫn này được thiết kế cho những người thích học bằng cách thực hành và muốn nhanh chóng thử tạo ra thứ gì đó hữu hình. Nếu bạn thích học từng khái niệm từng bước, hãy bắt đầu bằng cách Mô tả UI.

Hướng dẫn được chia thành nhiều phần:

Bạn đang xây dựng cái gì?

Trong hướng dẫn này, bạn sẽ xây dựng một trò chơi ô ăn quan tương tác bằng React.

Bạn có thể xem nó trông như thế nào khi hoàn thành ở đây:

import { useState } from 'react';

function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

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 (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});

return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}

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;
}

Nếu bạn chưa hiểu đoạn mã hoặc nếu bạn chưa quen với cú pháp của đoạn mã, đừ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 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 được đánh số ở bên phải bảng trò chơi. Danh sách này cung cấp cho bạn lịch sử của tất cả các nước đi đã diễn ra trong trò chơi và được cập nhật khi trò chơi tiến triển.

Sau khi bạn đã chơi trò chơi tic-tac-toe đã hoàn thành, hãy tiếp tục cuộn. Bạn sẽ bắt đầu với một mẫu đơ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

Trong trình soạn thảo mã trực tiếp bên dưới, hãy nhấp vào Fork ở góc trên bên phải để mở trình soạn thảo trong một tab mới bằng trang web CodeSandbox. CodeSandbox cho phép bạn viết mã trong trình duyệt và xem trước cách người dùng sẽ thấy ứng dụng bạn đã tạo. Tab mới sẽ hiển thị một ô vuông trống và mã khởi đầu cho hướng dẫn này.

export default function Square() {
return <button className="square">X</button>;
}

Ghi chú

Bạn cũng có thể làm theo hướng dẫn này bằng cách sử dụng môi trường phát triển cục bộ của bạn. Để làm như vậy, bạn cần:

  1. Cài đặt Node.js
  2. Trong tab CodeSandbox mà bạn đã mở trước đó, hãy nhấn nút góc trên cùng bên trái để mở menu, sau đó chọn Tải xuống Sandbox trong menu đó để tải xuống tệp lưu trữ cục bộ
  3. Giải nén tệp lưu trữ, sau đó mở terminal và cdvào thư mục bạn đã giải nén
  4. Cài đặt các phụ thuộc vớinpm install
  5. Chạy npm startđể khởi động máy chủ cục bộ và làm theo lời nhắc để xem mã đang chạy trong trình duyệt

Nếu bạn gặp khó khăn, đừng để điều này ngăn cản bạn! Hãy theo dõi trực tuyến và thử thiết lập cục bộ lại sau.

Tổng quan

Bây giờ bạn đã thiết lập xong, chúng ta hãy cùng tìm hiểu tổng quan về React!

Kiểm tra mã khởi động

Trong CodeSandbox bạn sẽ thấy ba phần chính:

CodeSandbox với mã khởi đầu

  1. Phần Tệp có danh sách các tệp như App.jsindex.jsstyles.cssvà một thư mục có tênpublic
  2. Trình soạn thảo mã nơi bạn sẽ thấy mã nguồn của tệp bạn đã chọn
  3. Phần trình duyệt nơi bạn sẽ thấy mã bạn đã viết sẽ được hiển thị như thế nào

Tệp App.jsphải được chọn trong phần Tệp . Nội dung của tệp đó trong trình soạn thảo mã phải là:

export default function Square() {
return <button className="square">X</button>;
}

Phần trình duyệt sẽ hiển thị một hình vuông có chữ X bên trong như thế này:

hình vuông có dấu x

Bây giờ chúng ta hãy xem xét các tập tin trong mã khởi động.

App.js 

Mã trong App.jstạo ra một thành phần . Trong React, một thành phần là một đoạn mã có thể tái sử dụng đại diện cho một phần của giao diện người dùng. Các thành phần được sử dụng để hiển thị, quản lý và cập nhật các thành phần UI trong ứng dụng của bạn. Hãy xem từng dòng thành phần để xem điều gì đang diễn ra:

export default function Square() {
return <button className="square">X</button>;
}

Dòng đầu tiên định nghĩa một hàm có tên là Square. exportTừ khóa JavaScript làm cho hàm này có thể truy cập được bên ngoài tệp này. defaultTừ khóa cho các tệp khác sử dụng mã của bạn biết rằng đó là hàm chính trong tệp của bạn.

export default function Square() {
return <button className="square">X</button>;
}

Dòng thứ hai trả về một nút. returnTừ khóa JavaScript có nghĩa là bất kỳ thứ gì theo sau được trả về dưới dạng giá trị cho người gọi hàm. <button>là một phần tử JSX . Một phần tử JSX là sự kết hợp giữa mã JavaScript và thẻ HTML mô tả những gì bạn muốn hiển thị. className="square"là một thuộc tính hoặc prop của nút cho CSS biết cách định dạng nút. Xlà văn bản hiển thị bên trong nút và </button>đóng phần tử JSX để chỉ ra rằng bất kỳ nội dung nào theo sau không được đặt bên trong nút.

styles.css 

Nhấp vào tệp được gắn nhãn styles.csstrong phần Tệp của CodeSandbox. Tệp 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 ( *body) định nghĩa kiểu của các phần lớn trong ứng dụng của bạn trong khi .squarebộ chọn định nghĩa kiểu của bất kỳ thành phần nào có classNamethuộc tính được đặt thành square. Trong mã của bạn, điều đó sẽ khớp với nút từ thành phần Square của bạn trong App.jstệp.

index.js 

Nhấp vào tệp được gắn nhãn index.jstrong phần Tệp của CodeSandbox. Bạn sẽ không chỉnh sửa tệp này trong suốt hướng dẫn nhưng đây là cầu nối giữa thành phần bạn đã tạo trong App.jstệp và trình duyệt web.

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';

import App from './App';

Các dòng 1-5 tổng hợp tất cả các phần cần thiết:

  • Phản ứng
  • Thư viện React để giao tiếp với trình duyệt web (React DOM)
  • các kiểu cho các thành phần của bạn
  • thành phần bạn đã tạo trong App.js.

Phần còn lại của tệp sẽ tập hợp tất cả các phần lại với nhau và đưa sản phẩm cuối cùng vào index.htmltrong publicthư mục.

Xây dựng bảng

Chúng ta 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ỉ có một ô vuông duy nhất, nhưng bạn cần chín ô vuông! Nếu bạn chỉ thử sao chép và dán ô vuông của mình để tạo thành hai ô vuông như thế này:

export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}

Bạn sẽ nhận được lỗi này:

Console

/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX Fragment <>...</>?

Các thành phần React cần trả về một phần tử JSX duy nhất chứ không phải nhiều phần tử JSX liền kề như hai nút. Để khắc phục điều này, bạn có thể sử dụng Fragments ( <></>) để bọc nhiều phần tử JSX liền kề như sau:

export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}

Bây giờ bạn sẽ thấy:

hai ô vuông có dấu x

Tuyệt! Bây giờ bạn chỉ cần sao chép-dán một vài lần để thêm chín ô vuông và…

chín ô vuông được tô đầy x trên một đường thẳng

Ôi không! Các ô vuông đều nằm trên một dòng, không phải trong lưới như bạn cần cho bảng của chúng tôi. Để khắc phục điều này, bạn sẽ cần nhóm các ô vuông của mình thành các hàng với divs và thêm một số lớp CSS. Trong khi thực hiện, bạn sẽ cung cấp cho mỗi ô vuông một số để đảm bảo bạn biết vị trí hiển thị của từng ô vuông.

Trong App.jstệp, hãy cập nhật Squarethành phần để trông như thế này:

export default function Square() {
return (
<>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</>
);
}

CSS được định nghĩa trong styles.csscác kiểu div với className. board-rowBây giờ bạn đã nhóm các thành phần của mình thành các hàng với kiểu divs, bạn đã có bàn cờ caro của mình:

bảng ô ăn quan được điền đầy đủ các số từ 1 đến 9

Nhưng bây giờ bạn gặp vấn đề. Thành phần có tên Square, của bạn thực sự không còn là hình vuông nữa. Hãy sửa lỗi đó bằng cách đổi tên thành Board:

export default function Board() {
//...
}

Lúc này mã của bạn sẽ trông giống thế này:

export default function Board() {
return (
<>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</>
);
}

Ghi chú

Psssst… Gõ nhiều quá! Bạn có thể sao chép và dán mã từ trang này. Tuy nhiên, nếu bạn muốn thử thách một chút, chúng tôi khuyên bạn chỉ nên sao chép mã mà bạn đã tự nhập ít nhất một lần.

Truyền dữ liệu qua props

Tiếp theo, bạn sẽ muốn thay đổi giá trị của một ô vuông từ rỗng thành “X” khi người dùng nhấp vào ô vuông. Với cách bạn đã xây dựng bảng cho đến nay, bạn sẽ cần sao chép-dán mã 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 thành phần của React cho phép bạn tạo một thành phần có thể tái sử dụng để tránh mã lộn xộn, trùng lặp.

Đầu tiên, bạn sẽ sao chép dòng xác định ô vuông đầu tiên ( <button className="square">1</button>) từ Boardthành phần của bạn vào một Squarethành phần mới:

function Square() {
return <button className="square">1</button>;
}

export default function Board() {
// ...
}

Sau đó, bạn sẽ cập nhật thành phần Board để hiển thị Squarethành phần đó bằng cú pháp JSX:

// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}

Lưu ý rằng không giống như trình duyệt div, các thành phần của riêng bạn Boardphải Squarebắt đầu bằng chữ in hoa.

Chúng ta hãy cùng xem nhé:

bảng một đầy

Ôi không! Bạn đã mất các ô vuông được đánh số 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 phải có từ thành phần cha ( Board) đến thành phần con ( Square).

Cập nhật Squarethành phần để đọc valueprop mà bạn sẽ truyền từ Board:

function Square({ value }) {
return <button className="square">1</button>;
}

function Square({ value })cho biết thành phần Square có thể được truyền một prop có tên là value.

Bây giờ bạn muốn hiển thị điều đó valuethay vì 1bên trong mỗi ô vuông. Hãy thử làm như thế này:

function Square({ value }) {
return <button className="square">value</button>;
}

Ồ, đây không phải là điều bạn muốn:

bảng giá trị đầy đủ

Bạn muốn render biến JavaScript được gọi valuetừ thành phần của bạn, 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 valuetrong JSX như sau:

function Square({ value }) {
return <button className="square">{value}</button>;
}

Bây giờ, bạn sẽ thấy một bảng trống:

bảng trống

Điều này là do Boardthành phần chưa truyền valueprop cho từng Squarethành phần mà nó kết xuất. Để khắc phục, bạn sẽ thêm valueprop vào từng Squarethành phần được Boardthành phần kết xuất:

export default function Board() {
return (
<>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</>
);
}

Bây giờ bạn sẽ lại thấy lưới số:

bảng ô ăn quan được điền đầy đủ các số từ 1 đến 9

function Square({ value }) {
return <button className="square">{value}</button>;
}

export default function Board() {
return (
<>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</>
);
}

Tạo thành phần tương tác

Hãy điền Squarethành phần bằng an Xkhi bạn nhấp vào nó. Khai báo một hàm được gọi handleClickbên trong Square. Sau đó, thêm onClickvào props của nút phần tử JSX được trả về từ Square:

function Square({ value }) {
function handleClick() {
console.log('clicked!');
}

return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}

Nếu bạn nhấp vào một ô vuông ngay bây giờ, bạn sẽ thấy một bản ghi ghi "clicked!"trong tab Console ở cuối phần Browser trong CodeSandbox. Nhấp vào ô vuông nhiều lần sẽ ghi "clicked!"lại. Các bản ghi console 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 bản ghi đầu tiên của mình "clicked!".

Ghi chú

Nếu bạn đang làm theo hướng dẫn này bằng môi trường phát triển cục bộ 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).

Bước tiếp theo, bạn muốn thành phần Square “ghi nhớ” rằng nó đã được nhấp và điền vào đó một dấu “X”. Để “ghi nhớ” mọi thứ, các thành phần sử dụng state .

React cung cấp một hàm đặc biệt useStatemà bạn có thể gọi từ thành phần của mình để cho phép nó "ghi nhớ" mọi thứ. Hãy lưu trữ giá trị hiện tại của Squaretrạng thái in và thay đổi nó khi Squaređược nhấp vào.

Nhập useStateở đầu tệp. Xóa valueprop khỏi Squarethành phần. Thay vào đó, thêm một dòng mới vào đầu lệnh Squaregọi useState. Yêu cầu nó trả về một biến trạng thái có tên là value:

import { useState } from 'react';

function Square() {
const [value, setValue] = useState(null);

function handleClick() {
//...

valuelưu trữ giá trị và setValuelà một hàm có thể được sử dụng để thay đổi giá trị. Giá trị nullđược truyền vào useStateđược sử dụng làm giá trị ban đầu cho biến trạng thái này, vì vậy valueở đây bắt đầu bằng null.

Squarethành phần không còn chấp nhận prop nữa nên bạn sẽ xóa valueprop khỏi tất cả chín thành phần Square được tạo bởi thành phần Board:

// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}

Bây giờ bạn sẽ thay đổi Squaređể hiển thị "X" khi nhấp vào. Thay thế trình console.log("clicked!");xử lý sự kiện bằng setValue('X');. Bây giờ Squarethành phần của bạn trông như thế này:

function Square() {
const [value, setValue] = useState(null);

function handleClick() {
setValue('X');
}

return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}

Bằng cách gọi sethàm này từ một onClicktrình xử lý, bạn đang yêu cầu React render lại hàm đó Squarebất cứ khi nào nó <button>được nhấp vào. Sau khi cập nhật, Square's valuesẽ là 'X', vì vậy bạn sẽ thấy "X" trên bảng trò chơi. Nhấp vào bất kỳ ô vuông nào và "X" sẽ hiển thị:

thêm xes vào bảng

Mỗi Square có trạng thái riêng: trạng thái valueđược lưu trữ trong mỗi Square hoàn toàn độc lập với các trạng thái khác. Khi bạn gọi một sethàm trong một thành phần, React cũng tự động cập nhật các thành phần con bên trong.

Sau khi thực hiện những thay đổi trên, mã của bạn sẽ trông như thế này:

import { useState } from 'react';

function Square() {
const [value, setValue] = useState(null);

function handleClick() {
setValue('X');
}

return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}

export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}

Công cụ phát triển React

React DevTools cho phép bạn kiểm tra props và trạng thái của các thành phần React. Bạn có thể tìm thấy tab React DevTools ở cuối phần trình duyệt trong CodeSandbox:

React DevTools trong CodeSandbox

Để kiểm tra một thành phần cụ thể trên màn hình, hãy sử dụng nút ở góc trên bên trái của React DevTools:

Chọn các thành phần trên trang bằng React DevTools

Ghi chú

Đối với phát triển cục bộ, React DevTools có sẵn dưới dạng tiện ích mở rộng cho trình duyệt Chrome , FirefoxEdge . Cài đặt nó và tab Components sẽ xuất hiện trong Developer Tools của trình duyệt dành cho các trang web sử dụng React.

Hoàn thành trò chơi

Đế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 phải thay phiên nhau đặt "X" và "O" trên bảng, và bạn cần một cách để xác định người chiến thắng.

Nâng trạng thái lên

Hiện tại, mỗi Squarethành phần duy trì một phần trạng thái của trò chơi. Để kiểm tra người chiến thắng trong trò chơi ô ăn quan, Boardbằng cách nào đó cần phải biết trạng thái của từng thành phần trong số 9 Squarethành phần.

Bạn sẽ tiếp cận điều đó như thế nào? Lúc đầu, bạn có thể đoán rằng Boardcần phải "hỏi" each Squaređể biết Squaretrạng thái của 's đó. Mặc dù về mặt kỹ thuật, cách tiếp cận này khả thi trong React, nhưng chúng tôi không khuyến khích vì mã sẽ trở nên khó hiểu, dễ bị lỗi và khó tái cấu trúc. Thay vào đó, cách tiếp cận tốt nhất là lưu trữ trạng thái của trò chơi trong Boardthành phần cha thay vì trong each Square. BoardThành phần có thể cho each biết Squarenội dung hiển thị 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 thành phần con hoặc để hai thành phần con giao tiếp với nhau, hãy khai báo trạng thái chung trong thành phần cha của chúng. Thành phần cha có thể truyền trạng thái đó trở lại thành phần con thông qua props. Điều này giúp các thành phần con đồng bộ với nhau và với thành phần cha của chúng.

Việc đưa trạng thái vào thành phần cha là việc thường gặp khi các thành phần React được tái cấu trúc.

Hãy tận dụng cơ hội này để thử nghiệm. Chỉnh sửa Boardthành phần để khai báo một biến trạng thái có tên squaresmặc định là một mảng gồm 9 giá trị null tương ứng với 9 ô vuông:

// ...
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. useState()Lệnh gọi xung quanh nó khai báo một squaresbiến trạng thái 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 vào bảng sau đó, mảng squaressẽ trông như thế này:

['O', null, 'X', 'X', 'X', 'O', 'O', null, null]

Bây giờ thành phần của bạn Boardcần truyền valueprop xuống từng thành phần Squaremà nó kết xuất:

export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<Square value={squares[1]} />
<Square value={squares[2]} />
</div>
<div className="board-row">
<Square value={squares[3]} />
<Square value={squares[4]} />
<Square value={squares[5]} />
</div>
<div className="board-row">
<Square value={squares[6]} />
<Square value={squares[7]} />
<Square value={squares[8]} />
</div>
</>
);
}

Tiếp theo, bạn sẽ chỉnh sửa Squarethành phần để nhận valueprop từ thành phần Board. Điều này sẽ yêu cầu xóa theo dõi trạng thái riêng của thành phần Square valuevà prop của nút onClick:

function Square({value}) {
return <button className="square">{value}</button>;
}

Lúc này bạn sẽ thấy một bàn cờ ca-rô trống rỗng:

bảng trống

Và mã của bạn sẽ trông như thế này:

import { useState } from 'react';

function Square({ value }) {
return <button className="square">{value}</button>;
}

export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<Square value={squares[1]} />
<Square value={squares[2]} />
</div>
<div className="board-row">
<Square value={squares[3]} />
<Square value={squares[4]} />
<Square value={squares[5]} />
</div>
<div className="board-row">
<Square value={squares[6]} />
<Square value={squares[7]} />
<Square value={squares[8]} />
</div>
</>
);
}

Mỗi ô vuông bây giờ sẽ nhận được một valueđạo cụ có thể là 'X', 'O', hoặc nullcho các ô vuông trống.

Tiếp theo, bạn cần thay đổi những gì xảy ra khi Squarenhấp vào a. BoardThành phần hiện duy trì ô vuông nào được tô. Bạn sẽ cần tạo một cách để Squarecập nhật Boardtrạng thái 's. Vì trạng thái là riêng tư đối với thành phần định nghĩa nó, bạn không thể cập nhật Boardtrạng thái 's trực tiếp từ Square.

Thay vào đó, bạn sẽ truyền một hàm từ Boardthành phần này sang Squarethành phần khác và bạn sẽ phải Squaregọi hàm đó khi nhấp vào hình vuông. Bạn sẽ bắt đầu với hàm mà Squarethành phần sẽ gọi khi nhấp vào. Bạn sẽ gọi hàm đó onSquareClick:

function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

Tiếp theo, bạn sẽ thêm onSquareClickhàm vào Squarethuộc tính của thành phần:

function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

Bây giờ bạn sẽ kết nối onSquareClickprop với một hàm trong Boardthành phần mà bạn sẽ đặt tên là handleClick. Để kết nối onSquareClickvới handleClickbạn sẽ truyền một hàm vào onSquareClickprop của Squarethành phần đầu tiên:

export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));

return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}

Cuối cùng, bạn sẽ định nghĩa handleClickhàm bên trong thành phần Board để cập nhật squaresmảng lưu giữ trạng thái của board:

export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));

function handleClick() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}

return (
// ...
)
}

Hàm này handleClicktạo một bản sao của squaresmảng ( nextSquares) bằng phương thức JavaScript slice()Array. Sau đó, handleClickcập nhật nextSquaresmảng để thêm Xvào ô vuông đầu tiên ( [0]index ).

Gọi setSquareshàm cho phép React biết trạng thái của thành phần đã thay đổi. Điều này sẽ kích hoạt việc render lại các thành phần sử dụng squarestrạng thái ( Board) cũng như các thành phần con của nó (các Squarethành phần tạo nên bảng).

Ghi chú

JavaScript hỗ trợ closures , nghĩa là một hàm bên trong (ví dụ handleClick) có thể truy cập vào các biến và hàm được định nghĩa trong một hàm bên ngoài (ví dụ Board). handleClickHàm có thể đọc squarestrạng thái và gọi setSquaresphương thức vì cả hai đều được định nghĩa bên trong hàm Board.

Bây giờ bạn có thể thêm X vào bảng… nhưng chỉ vào ô vuông trên cùng bên trái. handleClickHàm của bạn được mã hóa cứng để cập nhật chỉ mục cho ô vuông trên cùng 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 đối số ivào handleClickhàm lấy chỉ mục của ô vuông để cập nhật:

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 nó iđến handleClick. Bạn có thể thử đặt onSquareClickprop của square handleClick(0)trực tiếp trong JSX như thế này, nhưng nó sẽ không hoạt động:

<Square value={squares[0]} onSquareClick={handleClick(0)} />

Đây là lý do tại sao điều này không hiệu quả. Cuộc handleClick(0)gọi sẽ là một phần của việc kết xuất thành phần bảng. Bởi vì handleClick(0)thay đổi trạng thái của thành phần bảng bằng cách gọi setSquares, toàn bộ thành phần bảng của bạn sẽ được kết xuất lại. Nhưng điều này handleClick(0)lại chạy, dẫn đến một vòng lặp vô hạn:

Console

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 sớm hơn?

Khi bạn truyền onSquareClick={handleClick}, bạn đã truyền handleClickhàm xuống như một prop. Bạn không gọi nó! Nhưng bây giờ bạn đang gọi hàm đó ngay lập tức—hãy chú ý đến 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 handleClickcho đến khi người dùng nhấp chuột!

Bạn có thể sửa lỗi này bằng cách tạo một hàm như handleFirstSquareClickthế này calls handleClick(0), một hàm như handleSecondSquareClickthế này calls handleClick(1), v.v. Bạn sẽ truyền (thay vì call) các hàm này xuống dưới dạng props như onSquareClick={handleFirstSquareClick}. Điều này sẽ giải quyết được vòng lặp vô hạn.

Tuy nhiên, việc định nghĩa chín hàm khác nhau và đặt tên cho từng hàm là quá dài dòng. Thay vào đó, hãy làm như sau:

export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}

Lưu ý cú pháp mới () =>. Đây () => handleClick(0)là một hàm mũi tên, là cách ngắn hơn để định nghĩa hàm. Khi nhấp vào hình vuông, mã sau =>"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 khác để gọi handleClicktừ các hàm mũi tên bạn truyền qua. Đảm bảo rằng đối số cho mỗi lần gọi của handleClicktương ứng với chỉ số của ô vuông chính xác:

export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
};

Bây giờ bạn có thể 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:

điền X vào b�ảng

Nhưng lần này mọi hoạt động quản lý trạng thái đều được xử lý bởi Boardthành phần!

Mã của bạn sẽ trông như thế này:

import { useState } from 'react';

function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

Bây giờ, khi việc xử lý trạng thái của bạn nằm trong Boardthành phần, Boardthành phần cha sẽ truyền props cho các Squarethành phần con để chúng có thể được hiển thị chính xác. Khi nhấp vào Square, thành phần con Squaresẽ yêu cầu thành Boardphần cha cập nhật trạng thái của bảng. Khi Boardtrạng thái của 's thay đổi, cả Boardthành phần và mọi thành phần con đều Squaretự động kết xuất lại. Giữ nguyên trạng thái của tất cả các ô vuông trong Boardthành phần sẽ cho phép thành phần xác định người chiến thắng trong tương lai.

Hãy cùng 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 cùng bên trái trên bảng của bạn để thêm Xvào:

  1. Nhấp vào ô vuông trên cùng bên trái sẽ chạy hàm mà buttonnhận được như onClickprop của nó từ SquareSquareThành phần đã nhận được hàm đó như onSquareClickprop của nó từ BoardBoardThành phần đã định nghĩa hàm đó trực tiếp trong JSX. Nó gọi handleClickvới đối số là 0.
  2. handleClicksử dụng đối số ( 0) để cập nhật phần tử đầu tiên của squaresmảng từ nullđến X.
  3. Trạng squaresthái của Boardthành phần đã được cập nhật, do đó Boardvà tất cả các thành phần con của nó sẽ được render lại. Điều này khiến valueprop của Squarethành phần có chỉ mục 0thay đổi từ nullthành X.

Cuối cùng, người dùng sẽ thấy ô vuông trên cùng bên trái đã chuyển từ trống sang có hình Xsau khi nhấp vào.

Ghi chú

<button>Thuộc tính của phần tử DOM onClickcó ý nghĩa đặc biệt đối với React vì nó là một thành phần tích hợp. Đối với các thành phần tùy chỉnh như Square, việc đặt tên tùy thuộc vào bạn. Bạn có thể đặt bất kỳ tên nào cho Squareprop onSquareClickhoặc hàm Board's handleClickvà mã sẽ hoạt động giống nhau. Trong React, thông thường là sử dụng onSomethingtên cho các prop biểu diễn các sự kiện và handleSomethingcho các định nghĩa hàm xử lý các sự kiện đó.

Tại sao tính bất biến lại quan trọng

Lưu ý cách trong handleClick, bạn gọi .slice()để tạo một bản sao của squaresmảng thay vì sửa đổi mảng hiện có. Để giải thích lý do, 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 để thay đổi dữ liệu. Cách thứ nhất là đột biến dữ liệu bằng cách trực tiếp thay đổi giá trị của dữ liệu. Cách thứ hai là thay thế dữ liệu bằng một bản sao mới có những thay đổi mong muốn. Sau đây là những gì sẽ trông như thế nào nếu bạn đột biến squaresmảng:

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];

Và đây là hình ảnh trông như thế nào nếu bạn thay đổi dữ liệu mà không làm thay đổi squaresmảng:

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`

Kết quả thì giống nhau nhưng bằng cách không đột biến (thay đổi dữ liệu cơ bản) trực tiếp, bạn sẽ đạt được một số lợi ích.

Tính bất biến giúp 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 trở lại" các nước đi trước đó. Chức năng này không dành riêng cho trò chơi—khả năng hoàn tác và làm lại một số hành động nhất định là yêu cầu chung đối với 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 dữ liệu trước đó và sử dụng lại chúng sau này.

Ngoài ra còn 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 thành phần con sẽ tự động render lại khi trạng thái của thành phần cha thay đổi. Điều này bao gồm cả các thành phần con không bị ảnh hưởng bởi thay đổi. Mặc dù bản thân việc render lại không đáng chú ý đối với người dùng (bạn không nên chủ động cố gắng tránh nó!), bạn có thể muốn bỏ qua việc render lại 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 giúp các thành phần dễ dàng so sánh xem dữ liệu của chúng có thay đổi hay không. Bạn có thể tìm hiểu thêm về cách React chọn thời điểm render lại một thành phần trong tài memoliệu tham khảo API .

Lần lượt

Bây giờ đã đến lúc sửa một lỗi lớn trong trò chơi ô ăn quan này: không thể đánh dấu chữ “O” 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 trạng thái khác vào thành phần Board:

function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));

// ...
}

Mỗi lần một người chơi di chuyển, xIsNext(một boolean) sẽ được lật để xác định người chơi nào đi tiếp theo và trạng thái của trò chơi sẽ được lưu. Bạn sẽ cập nhật hàm Board's handleClickđể lật giá trị của xIsNext:

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ẽ xen kẽ giữa XO, như mong đợi!

Nhưng khoan đã, có một vấn đề. Hãy thử nhấp vào cùng một ô vuông nhiều lần:

O ghi đè lên X

Được Xghi đè bằng dấu O! Mặc dù điều này sẽ tạo nên sự thú vị cho trò chơi, nhưng hiện tại chúng ta vẫn sẽ tuân thủ theo các quy tắc ban đầu.

Khi bạn đánh dấu một ô vuông bằng a Xhoặc an, Otrước tiên bạn không kiểm tra xem ô vuông đó đã có giá trị a Xhoặc chưa. Bạn có thể sửa lỗi này bằng cách trả về sớm . Bạn sẽ kiểm tra xem ô vuông đó đã có a hoặc an chưa . Nếu ô vuông đã được điền, bạn sẽ vào hàm sớm—trước khi nó cố gắng cập nhật trạng thái bảng.O``X``O``return``handleClick

function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}

Bây giờ bạn chỉ có thể thêm X's hoặc O's vào ô trống! Sau đây là những gì mã của bạn sẽ trông như thế nào tại thời điểm này:

import { useState } from 'react';

function Square({value, onSquareClick}) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

Tuyên bố người chiến thắng

Bây giờ người chơi có thể thay phiên nhau, bạn sẽ muốn hiển thị khi nào trò chơi thắng và không còn lượt nào nữa. Để làm điều này, bạn sẽ thêm một hàm trợ giúp có tên là hàm calculateWinnernày lấy một mảng gồm 9 ô vuông, kiểm tra xem có người chiến thắng không và trả về 'X', 'O', hoặc nulltùy theo trường hợp. Đừng quá lo lắng về calculateWinnerhàm này; nó không dành riêng cho React:

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;
}

Ghi chú

Không quan trọng bạn định nghĩa calculateWinnertrước hay sau dấu Board. Hãy đặt nó ở cuối để bạn không phải cuộn qua nó mỗi khi chỉnh sửa các thành phần của mình.

Bạn sẽ gọi calculateWinner(squares)hàm Boardcủa thành phần handleClickđể kiểm tra xem người chơi có thắng không. 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 có nhấp vào ô vuông đã có Xhoặc và O. Chúng tôi muốn trả về sớm trong cả hai trường hợp:

function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}

Để cho người chơi biết khi nào trò chơi kết thúc, bạn có thể hiển thị văn bản như "Người chiến thắng: X" hoặc "Người chiến thắng: O". Để làm như vậy, bạn sẽ thêm một statusphần vào Boardthành phần. Trạng thái sẽ hiển thị người chiến 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 của người chơi nào tiếp theo:

export default function Board() {
// ...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}

return (
<>
<div className="status">{status}</div>
<div className="board-row">
// ...
)
}

Xin chúc mừng! Bây giờ bạn đã có một trò chơi tic-tac-toe hoạt động. Và bạn vừa học được những điều cơ bản của React. Vậy là bạn là người chiến thắng thực sự ở đây. Mã sẽ trông như thế này:

import { useState } from 'react';

function Square({value, onSquareClick}) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

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;
}

Thêm du hành thời gian

Bài tập cuối cùng là cho phép bạn "quay ngược thời gian" về những nước đi trước đó trong trò chơi.

Lưu trữ lịch sử di chuyển

Nếu bạn đột biến squaresmảng, việc thực hiện du hành thời gian sẽ rất khó khăn.

Tuy nhiên, bạn đã từng slice()tạo một bản sao mới của squaresmảng sau mỗi lần di chuyển và coi nó là 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 squaresmảng và điều hướng giữa các lượt đã diễn ra.

Bạn sẽ lưu trữ squarescác mảng trước đó trong một mảng khác có tên là history, mà bạn sẽ lưu trữ dưới dạng một biến trạng thái mới. historyMảng này biểu diễn tất cả các trạng thái của bàn cờ, từ nước đi đầu tiên đến nước đi cuối cùng, và có hình dạng như sau:

[
// 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 nhà nước lên, một lần nữa

Bây giờ bạn sẽ viết một thành phần cấp cao nhất mới được gọi Gameđể hiển thị danh sách các nước đi trước đó. Đó là nơi bạn sẽ đặt historytrạng thái chứa toàn bộ lịch sử trò chơi.

Đặt historytrạng thái vào Gamethành phần sẽ cho phép bạn xóa trạng squaresthái khỏi thành phần con của nó Board. Giống như bạn đã "nâng trạng thái lên" từ Squarethành phần vào Boardthành phần, giờ đây bạn sẽ nâng nó lên từ thành phần Boardcấp cao nhất Game. Điều này cung cấp cho Gamethành phần toàn quyền kiểm soát Boarddữ liệu 's và cho phép nó hướng dẫn Boardđể hiển thị các lượt trước đó từ history.

Đầu tiên, thêm một Gamethành phần với export default. Yêu cầu nó hiển thị Boardthành phần và một số đánh dấu:

function Board() {
// ...
}

export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}

Lưu ý rằng bạn đang xóa export defaultcác từ khóa trước function Board() {khai báo và thêm chúng trước function Game() {khai báo. Điều này cho biết index.jstệp của bạn sử dụng Gamethành phần làm thành phần cấp cao nhất thay vì Boardthành phần của bạn. Các divs bổ sung được trả về bởi Gamethành phần đang tạo chỗ cho thông tin trò chơi mà bạn sẽ thêm vào bảng sau.

Thêm một số trạng thái vào Gamethành phần để theo dõi người chơi tiếp theo và lịch sử di chuyển:

export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...

Lưu ý rằng [Array(9).fill(null)]mảng có một phần tử duy nhất, bản thân mảng này là mảng có 9 nullphần tử.

Để hiển thị các ô vuông cho nước đi hiện tại, bạn sẽ muốn đọc mảng ô vuông cuối cùng từ history. Bạn không cần useStatelàm điều này—bạn đã có đủ thông tin để tính toán trong quá trình hiển thị:

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 handlePlayhàm bên trong Gamethành phần sẽ được Boardthành phần gọi để cập nhật trò chơi. Truyền xIsNext, currentSquareshandlePlaylàm props cho Boardthành phần:

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 (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
//...
)
}

Hãy làm cho Boardthành phần được điều khiển hoàn toàn bởi các prop mà nó nhận được. Thay đổi Boardthành phần để lấy ba prop: xIsNext, squares, và một onPlayhàm mới Boardcó thể gọi với mảng squares đã cập nhật khi người chơi di chuyển. Tiếp theo, xóa hai dòng đầu tiên của Boardhàm gọi useState:

function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}

Bây giờ hãy thay thế các lệnh gọi setSquaresand trong thành phần bằng một lệnh gọi duy nhất đến hàm mới của bạn để thành phần có thể cập nhật khi người dùng nhấp vào hình vuông:setXIsNext``handleClick``Board``onPlay``Game``Board

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);
}
//...
}

Thành Boardphần được điều khiển hoàn toàn bởi các props được truyền cho nó bởi Gamethành phần. Bạn cần triển khai handlePlayhàm trong Gamethành phần để trò chơi hoạt động trở lại.

Nên làm gì handlePlaykhi được gọi? Hãy nhớ rằng Board từng gọi setSquaresvới một mảng đã cập nhật; bây giờ nó truyền squaresmảng đã cập nhật tới onPlay.

Hàm handlePlaycần cập nhật Gametrạng thái của ' để kích hoạt việc kết xuất lại, nhưng bạn không có setSquareshàm nào có thể gọi nữa—bây giờ bạn đang sử dụng historybiến trạng thái để lưu trữ thông tin này. Bạn sẽ muốn cập nhật historybằng cách thêm squaresmảng đã cập nhật dưới dạng mục nhập lịch sử mới. Bạn cũng muốn chuyển đổi xIsNext, giống như Board đã từng làm:

export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}

Tại đây, [...history, nextSquares]tạo một mảng mới chứa tất cả các mục trong history, theo sau là nextSquares. (Bạn có thể đọc ...history cú pháp lan truyền là “liệt kê tất cả các mục trong history”.)

Ví dụ, nếu historyis [[null,null,null], ["X",null,null]]nextSquaresis ["X",null,"O"]thì [...history, nextSquares]mảng mới sẽ là [[null,null,null], ["X",null,null], ["X",null,"O"]].

Tại thời điểm này, bạn đã di chuyển trạng thái để tồn tại trong Gamethành phần và UI sẽ hoạt động hoàn toàn, giống như trước khi tái cấu trúc. Sau đây là giao diện của mã tại thời điểm này:

import { useState } from 'react';

function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

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 (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}

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;
}

Hiển thị các động thái trước đó

Vì bạn đang ghi lại lịch sử chơi trò ô ăn quan nên giờ đây 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ư <button>là các đối tượng JavaScript thông thường; bạn có thể truyền chúng trong ứng dụng của mình. Để hiển thị 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 historycác move trong state, vì vậy bây giờ bạn cần chuyển đổi nó thành một mảng các phần tử React. Trong JavaScript, để chuyển đổi một mảng thành một mảng khác, bạn có thể sử dụng phương thức array map:

[1, 2, 3].map((x) => x * 2) // [2, 4, 6]

Bạn sẽ sử dụng mapđể chuyển đổi historycác bước di chuyển của mình thành các phần tử React biểu diễn các nút trên màn hình và hiển thị danh sách các nút để "nhảy" đến các bước di chuyển trước đó. Hãy cùng mapxem qua historythành phần Game:

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 (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});

return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}

Bạn có thể xem mã của bạn trông như thế nào bên dưới. Lưu ý rằng bạn sẽ thấy lỗi trong bảng điều khiển công cụ dành cho nhà phát triển có nội dung:

Console

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 ở phần tiếp theo.

import { useState } from 'react';

function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

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 (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});

return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}

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;
}

Cảnh báo: Mỗi phần tử con trong danh sách phải có một prop "key" duy nhất. Kiểm tra phương thức render của Game. Xem https://reactjs.org/link/warning-keys để biết thêm thông tin. tại li at Game (https://786946de.sandpack-bundler-4bw.pages.dev/src/App.js:109:53)

Hiển thị thêm

Khi bạn lặp qua historymảng bên trong hàm bạn đã truyền vào map, squaresđối số sẽ đi qua từng phần tử của history, và moveđối số sẽ đi qua từng chỉ mục 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 để hiển thị danh sách các bước di chuyển, bạn sẽ chỉ cần chỉ mục.)

Đố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 <li>chứa một nút <button>. Nút này có một onClicktrình xử lý gọi một hàm được gọi là jumpTo(mà bạn chưa triển khai).

Hiện tại, bạn sẽ thấy danh sách các động thái diễn ra trong trò chơi và lỗi trong bảng điều khiển công cụ dành cho nhà phát triển. Hãy cùng thảo luận về ý nghĩa của lỗi "key".

Chọn một chìa khóa

Khi bạn render một danh sách, React lưu trữ một số thông tin về từng mục danh sách được 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ừ

<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>

ĐẾN

<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>

Ngoài số lượng được cập nhật, một người đọc điều này có thể sẽ nói rằng bạn đã hoá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 định làm gì, vì vậy bạn cần chỉ định một thuộc tính khóa cho mỗi mục danh sách để phân biệt từng mục danh sách với các mục anh chị em của nó. Nếu dữ liệu của bạn đến từ cơ sở dữ liệu, thì ID cơ sở dữ liệu của Alexa, Ben và Claudia có thể được sử dụng làm khóa.

<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>

Khi một danh sách được render lại, React lấy khóa của từng mục danh sách và tìm kiếm các mục danh sách trước đó để tìm khóa khớp. Nếu danh sách hiện tại có khóa không tồn tại trước đó, React sẽ tạo một thành phần. Nếu danh sách hiện tại thiếu khóa đã tồn tại trong danh sách trước đó, React sẽ hủy thành phần trước đó. Nếu hai khóa khớp nhau, thành phần tương ứng sẽ được di chuyển.

Các khóa cho React biết về danh tính của từng thành phần, cho phép React duy trì trạng thái giữa các lần kết xuất lại. Nếu khóa của thành phần thay đổi, thành phần đó sẽ bị hủy và được tạo lại với trạng thái mới.

keylà 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 keythuộc tính và lưu trữ khóa trực tiếp trên phần tử được trả về. Mặc dù keycó vẻ như được truyền dưới dạng props, React tự động sử dụng keyđể quyết định thành phần nào sẽ cập nhật. Không có cách nào để một thành phần hỏi thành phần keycha của nó đã chỉ định gì.

Bạn nên chỉ định khóa thích 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ó khóa thích hợp, bạn có thể cân nhắc tái cấu trúc dữ liệu của mình để có.

Nếu không chỉ định khóa, React sẽ báo lỗi và sử dụng chỉ mục mảng làm khóa theo mặc định. Sử dụng chỉ mục mảng làm khóa sẽ gây ra vấn đề khi cố gắng sắp xếp lại các mục trong danh sách hoặc chèn/xóa các mục trong danh sách. Truyền rõ ràng sẽ key={i}làm im lặng lỗi nhưng có cùng vấn đề như chỉ mục mảng và không được khuyến khích trong hầu hết các trường hợp.

Khóa không cần phải là duy nhất trên toàn cục; chúng chỉ cần là duy nhất giữa các thành phần và các thành phần cùng cấp.

Thực hiện du hành thời gian

Trong lịch sử trò chơi tic-tac-toe, mỗi nước đi trước đó đều có một ID duy nhất liên quan đến nó: đó là số thứ 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, có thể sử dụng chỉ số nước đi làm chìa khóa.

Trong Gamehàm, bạn có thể thêm khóa là <li key={move}>, và nếu bạn tải lại trò chơi đã kết xuất, lỗi "khóa" của React sẽ biến mất:

const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
import { useState } from 'react';

function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

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 (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});

return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}

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;
}

Trước khi bạn có thể triển khai jumpTo, bạn cần Gamethành phần theo dõi bước nào mà người dùng hiện đang xem. Để thực hiện việc này, hãy định nghĩa một biến trạng thái mới có tên là currentMove, mặc định là 0:

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 jumpTohàm bên trong Gameđể cập nhật currentMove. Bạn cũng sẽ thiết lập xIsNextnếu truesố bạn đang thay đổi currentMovelà số chẵn.

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 hàm Game's handlePlayđược gọi khi bạn nhấp vào hình 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ừ thời điểm đó, bạn chỉ muốn giữ lại lịch sử cho đến thời điểm đó. Thay vì thêm nextSquaresafter all items ( ...spread syntax) vào history, bạn sẽ thêm nó sau all items vào history.slice(0, currentMove + 1)để bạn chỉ giữ lại phần lịch sử cũ đó.
  • Mỗi lần thực hiện một nước đi, bạn cần cập nhật currentMoveđể trỏ đến mục lịch sử mới nhất.
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 Gamethành phần để hiển thị nước đi hiện đang được chọn, thay vì luôn hiển thị nước đi cuối cùng:

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 ô ăn quan sẽ ngay lập tức cập nhật để hiển thị giao diện của bảng sau khi bước đó diễn ra.

import { useState } from 'react';

function Square({value, onSquareClick}) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

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 (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});

return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}

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;
}

Dọn dẹp cuối cùng

Nếu bạn xem xét mã rất kỹ, bạn có thể nhận thấy rằng xIsNext === truewhen currentMovelà chẵn và xIsNext === falsewhen currentMovelà 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ìm ra giá trị xIsNextcần có.

Không có lý do gì để bạn lưu trữ cả hai thứ này trong trạng thái. Trên thực tế, hãy luôn cố gắng tránh trạng thái dư thừa. Việc đơn giản hóa những gì bạn lưu trữ trong trạng thái sẽ giảm lỗi và giúp mã của bạn dễ hiểu hơn. Thay đổi Gameđể nó không lưu trữ xIsNextdưới dạng một biến trạng thái riêng biệt mà thay vào đó tìm ra nó dựa trên currentMove:

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 xIsNextkhai báo trạng thái hoặc các lệnh gọi đến setXIsNext. Bây giờ, không có cơ hội nào để xIsNextkhông đồng bộ với currentMove, ngay cả khi bạn mắc lỗi khi mã hóa các thành phần.

Kết thúc

Xin chúc mừng! Bạn đã tạo ra một trò chơi ô ăn quan:

  • Cho phép bạn chơi trò ô ăn quan,
  • Chỉ ra khi nào người chơi đã thắng trò chơi,
  • Lưu trữ lịch sử trò chơi khi trò chơi đang 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 trò chơi.

Làm tốt lắm! Chúng tôi hy vọng bây giờ bạn cảm thấy mình đã nắm bắt khá rõ cách React hoạt động.

Xem kết quả cuối cùng tại đây:

import { useState } from 'react';

function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}

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 (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

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 (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});

return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}

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;
}

Nếu bạn có thêm thời gian hoặc muốn luyện tập các kỹ năng React mới của mình, sau đây là một số ý tưởng cải tiến mà bạn có thể thực hiện đối với trò chơi ô ăn quan, được liệt kê theo thứ tự độ khó tăng dần:

  1. Chỉ đối với nước đi hiện tại, hãy hiển thị “Bạn đang ở nước đi số…” thay vì một nút.
  2. Viết lại Boardđể sử dụng hai vòng lặp để tạo hình vuông thay vì mã hóa cứng chúng.
  3. Thêm nút chuyển đổi 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.
  4. Khi có người thắng, hãy đánh dấu ba ô vuông tạo nên chiến thắng (và khi không có ai thắng, hãy hiển thị thông báo về kết quả hòa).
  5. Hiển thị vị trí cho mỗi lần di chuyển theo định dạng (hàng, cột) trong danh sách lịch sử di chuyển.

Trong suốt hướng dẫn này, bạn đã đề cập đến các khái niệm React bao gồm các thành phần, thành phần, đạo cụ và trạng thái. 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 trò chơi, hãy xem Suy nghĩ trong React để xem các khái niệm React tương tự hoạt động như thế nào khi xây dựng giao diện người dùng của ứng dụng.