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:
- Thiết lập hướng dẫn sẽ cung cấp cho bạn điểm khởi đầu để thực hiện theo hướng dẫn.
- Tổng quan sẽ hướng dẫn bạn những kiến thức cơ bản về React: thành phần, thuộc tính và trạng thái.
- Hoàn thành trò chơi sẽ dạy cho bạn những kỹ thuật phổ biến nhất trong phát triển React.
- Việc thêm tính năng du hành thời gian sẽ giúp bạn hiểu sâu hơn về những điểm mạnh độc đáo của React.
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:
- Cài đặt Node.js
- 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ộ
- Giải nén tệp lưu trữ, sau đó mở terminal và
cdvào thư mục bạn đã giải nén - Cài đặt các phụ thuộc với
npm install - 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:

- Phần Tệp có danh sách các tệp như
App.js,index.js,styles.cssvà một thư mục có tênpublic - 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
- 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:

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 ( *và 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 ( <>và </>) để 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:

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à…

Ô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:

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é:

Ô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ạ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:

Đ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ố:

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.
Vì 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ị:

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:

Để 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:

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 , Firefox và Edge . 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:

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:
