آموزش: ساخت بازی دوز با React

در این آموزش شما یک بازی دوز کوچک میسازید. در این آموزش فرض بر این است که شما هیچ دانش قبلی در مورد React ندارید. تکنیکهایی که در این آموزش یاد میگیرید، روش های بنیادی برای ساخت برنامه های React است و درک کامل آن به شما فهم عمیقی از React میدهد.
این آموزش برای افرادی طراحی شده است که ترجیح میدهند از طریق کار کردن یاد بگیرند و میخواهند به سرعت چیزی ملموس بسازند. اگر یادگیری هر مفهوم را به صورت مرحله به مرحله ترجیح میدهید، با مطالعه توصیف رابط کاربری شروع کنید.
چه چیزی قرار است بسازید؟
در این آموزش، شما یک بازی دوز تعاملی با React خواهید ساخت.
نتیجه کار پس از اتمام این آموزش و ساخت بازی،به شکل زیر است:
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;
}
اگر کد بالا برایتان هنوز مفهومی ندارد، یا با نحوه نوشتن کد آشنا نیستید، نگران نباشید! هدف این آموزش کمک برای یادگیری React و نحوه نوشتن آن است.
پیشنهاد میکنیم قبل از ادامه با آموزش، بازی دوز ساخته شده که لینک آن در بالا آمده است را بررسی کنید. یکی از ویژگیهای بازی ساخته شده این است که لیستی شمارهدار در سمت راست صفحه بازی وجود دارد که تاریخچه تمامی حرکات انجام شده در بازی را نمایش میدهد و با پیشرفت بازی بهروز میشود.
پس از اینکه بازی دوز را بررسی کردید، مطالعه نحوه ساخت آن را شروع کنید. در این آموزش با یادگیری ساخت یک الگوی سادهتر شروع خواهید کرد. گام اول آماده کردن شما برای شروع به ساخت بازی کنید.
شروع آموزش
برای شروع می توانید روی لینک مشاهده نتیجه کد کلیک کنید تا ویرایشگر کد در پنجره جدید حاوی کدهای هر بخش برایتان باز شود، کد را تغییر دهید و نتیجه کدهای نوشته شده را مشاهده کنید.
export default function Square() {
return <button className="square">X</button>;
}
همچنین میتوانید با استفاده از کامپیوتر خود کدهای این آموزش را اجرا کنید. برای این کار باید:
- Node.js را نصب کنید.
- در تب CodeSandbox که قبلاً باز کردهاید، روی دکمه گوشه بالا-چپ کلیک کنید تا منو باز شود، و سپس Download Sandbox را انتخاب کنید و فایل آرشیو کدها را بروی سیستم خود دانلود کنید.
- آرشیو را از حالت فشرده خارج کنید، سپس یک ترمینال باز کرده و به دایرکتوری که از حالت فشرده خارج کردهاید بروید.
- وابستگیها را با دستور
npm install
نصب کنید. - با اجرای
npm start
یک سرور محلی اجرا می شود و با باز کردن آدرسی که به شما نمایش داده می شود، کد را در مرورگر خود اجرا کنید.
اگر جایی در مراحل بالا گیر کردید، نگذارید این موضوع شما را متوقف کند! به صورت آنلاین ادامه دهید و بعداً دوباره سعی کنید تا محیط توسعه محلی خود را راهاندازی کنید.
مروری بر کلیات
حالا که آماده شروع هستید، بیایید یک مروری بر React داشته باشیم!
بررسی کد ابتدایی
در CodeSandbox، شما سه بخش اصلی خواهید دید:

- بخش فایلها که شامل لیستی از فایلها مانند
App.js
،index.js
،styles.css
و یک پوشه به نامpublic
است. - ویرایشگر کد که در آن کد فایل انتخابی را مشاهده خواهید کرد.
- بخش مرورگر که در آن نتیجه کدی که نوشتهاید نمایش داده میشود.
اگر فایل App.js
در بخش فایلها انتخاب شده باشد. محتوای آن فایل در ویرایشگر کد به صورت زیر نمایش داده می شود:
export default function Square() {
return <button className="square">X</button>;
}
در بخش مرورگر باید یک مربع با علامت X نمایش داده شود، مانند این:

حالا بیایید نگاهی به فایلها بیاندازیم.
فایل App.js
کد App.js
تعریف یک کامپوننت است. در React، کامپوننت تکهای از کد با قابلیت استفاده مجدد است که بخشی از رابط کاربری را نمایش میدهد. کامپوننتها برای رندر، مدیریت و بهروزرسانی عناصر UI در اپلیکیشن استفاده میشوند. بیایید به خط به خط به این کامپوننت نگاهی بیندازیم تا ببینیم چه اتفاقی میافتد:
export default function Square() {
return <button className="square">X</button>;
}
خط اول یک تابع به نام Square
تعریف میکند. کلمه کلیدی export
در جاوا اسکریپت باعث میشود که این تابع در خارج از این فایل قابل دسترسی باشد. کلمه کلیدی default
به فایلهای دیگر که از کد شما استفاده میکنند میگوید که این تابع خروجی اصلی فایل شما است.
export default function Square() {
return <button className="square">X</button>;
}
خط دوم یک دکمه برمیگرداند. کلمه کلیدی return
در توابع جاوا اسکریپت هر چیزی را که بعد از آن بیاید به عنوان مقداری به جایی که تابع فراخوانی شده برگشت می دهد. <button>
یک عنصر JSX است. عنصر JSX می تواند ترکیبی از کد جاوا اسکریپت و تگهای HTML باشد و آنچه را که میخواهید نمایش دهید، توصیف میکند. className="square"
ویژگی یا prop دکمه است که با کلاس های CSS میگوید چگونه دکمه را استایل دهی شود. X
متنی است که درون دکمه نمایش داده میشود و </button>
انتهاب عنصر JSX را مشخص می کند تا محتوای بین تگ شروع و پایان فقط در دکمه نمایش داده شود.
فایل styles.css
در CodeSandbox روی فایل styles.css
در بخش فایلها کلیک کنید. در این فایل استایلهای اپلیکیشن React تعریف شده اند. دو انتخابگر CSS ای که اول آمده (*
و body
) استایل قسمتهای کلی اپلیکیشن را تعریف میکند در حالی که انتخابگر .square
استایل هر کامپوننتی را که ویژگی className
آن برابر square
تنظیم شده است را تعریف میکند. در کد بازی، این استایل به دکمه درون کامپوننت Square در فایل App.js
اختصاص داده شده است.
فایل index.js
روی فایل index.js
در بخش فایلها در CodeSandbox کلیک کنید. شما در طول آموزش ویرایشی بر روی این فایل نخواهید داشت اما این فایل پلی بین کامپوننت فایل App.js
و مرورگر وب است.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
خطوط ۱-۵ این کد تمام اجزا ضروری را با هم ترکیب میکنند:
- React
- کتابخانه React برای ارتباط با مرورگرهای وب (React DOM)
- استایلها برای کامپوننتهای شما
- کامپوننتی که در
App.js
ایجاد کردهاید.
باقیمانده فایل تمام اجزا را با هم ترکیب میکند و محصول نهایی را به index.html
در پوشه public
تزریق میکند.
ساخت برد
بیایید به App.js
برگردیم. فایلی که بقیه آموزش با آن سر و کار داریم.
در حال حاضر برد بازی تنها یک مربع است، اما شما به نه مربع نیاز دارید! اگر سعی کنید مربع اول را کپی کرده تا دو مربع بسازید مانند این:
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
شما با این خطا مواجه خواهید شد:
/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX Fragment <>...</>?
کامپوننتهای React باید فقط یک عنصر JSX برگردانند و نباید چندین عنصر JSX کنار هم مانند دو دکمه را برگردانند. برای رفع این مشکل میتوانید از Fragments (<>
و </>
) برای محاط کردن چندین عنصر JSX مجاور مانند کد زیر استفاده کنید:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
حالا خروجی باید این گونه باشد:

عالی! حالا فقط باید چند بار کپی-پیست کنید تا نه مربع را به بورد اضافه کنید و...

اوه نه! مربعها همه در یک خط هستند، نه در یک شبکه که ما برای برد به آن نیاز داریم. برای حل این مشکل باید مربعها را با div
به ردیفهایی تقسیم کنید و چند کلاس CSS به آن ها اضافه کنید. در حین کار، به هر مربع یک شماره دهید تا مطمئن شوید که هر مربع کجا نمایش داده میشود.
در فایل App.js
، کامپوننت Square
را به این شکل بهروزرسانی کنید:
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 تعریف شده در styles.css
divها را با className
از board-row
استایل میکند. حالا که کامپوننتهای خود را در ردیفها با divهای استایلدار گروهبندی کردهاید، شما برد دوز خود را دارید:

اما الان یک مشکل دارید. نام کامپوننت شما Square
است، در واقع دیگر یک مربع نیست. بیایید این مشکل را با تغییر نام آن به Board
اصلاح کنیم:
export default function Board() {
//...
}
در این مرحله کد شما باید چیزی شبیه به این باشد: مشاهده نتیجه در CodeSandBox
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>
</>
);
}
هی... این خیلی نیاز به تایپ کردن دارد! اشکالی ندارد که از این صفحه کد را کپی و پیست کنید. با این حال، اگر شما آماده یک چالش هستید، پیشنهاد میکنیم فقط کدهایی را که حداقل یک بار بهصورت دستی تایپ کردهاید کپی کنید.
انتقال دادهها از طریق props
در مرحله بعد، شما میخواهید زمانی که کاربر روی مربع کلیک میکند، مقدار یک مربع را از خالی به "X" تغییر دهید. با نحوهی ساخت برد تا کنون، شما باید کد بهروزرسانی مربع را نه بار کپی-پیست کنید (یک بار برای هر مربع)! به جای کپی-پیست کردن، معماری کامپوننت React به شما این اجازه را میدهد که یک کامپوننت قابل استفاده مجدد ایجاد کنید تا از کد تکراری و به هم ریخته جلوگیری کنید.
اول، شما خط تعریف اولین مربع خود (<button className="square">1</button>
) را از کامپوننت Board
به یک کامپوننت جدید Square
کپی خواهید کرد:
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
سپس کامپوننت Board را بهروزرسانی کنید تا از کد JSX برای نمایش کامپوننت Square
استفاده کند:
// ...
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>
</>
);
}
توجه داشته باشید که برخلاف divهای مرورگر، کامپوننتهای خود شما مانند Board
و Square
باید با حرف بزرگ شروع شوند.
بیایید نگاهی به آن بیاندازیم:

اوه نه! شما مربعهای شمارهداری که قبلاً داشتید را از دست دادید. حالا هر مربع عدد "1" را نمایش می دهد. برای رفع این مشکل، باید از props استفاده کنید تا مقداری را که هر مربع باید از کامپوننت والد (Board
) به فرزند (Square
) داشته باشد به آن منتقل کنید.
کامپوننت Square
را بهروزرسانی کنید تا ویژگی value
را که از Board
ارسال میکنید، بخواند:
function Square({ value }) {
return <button className="square">1</button>;
}
عبارت function Square({ value })
مشخص می کند که کامپوننت Square میتواند یک prop به نام value
دریافت کند.
حالا باید مقدار ویژگی value
را به جای 1
درون هر مربع نمایش دهید. سعی کنید این کار را اینگونه انجام دهید:
function Square({ value }) {
return <button className="square">value</button>;
}
اوه، اما این چیزی نیست که میخواستید:

شما میخواستید متغیر جاوا اسکریپت به نام value
را از کامپوننت خود رندر کنید، نه کلمه "value" را. برای استفاده از جاوااسکریپت در JSX، شما نیاز به علامت براکت دارید. علامت براکت را در JSX به دور value
مانند زیر اضافه کنید:
function Square({ value }) {
return <button className="square">{value}</button>;
}
هماکنون، باید یک برد خالی ببینید:

این به این دلیل است که کامپوننت Board
هنوز مقدار value
را به کامپوننت های Square
ارسال نکرده است. برای رفع این مشکل، باید ویژگی value
را به کامپوننت های Square
که توسط کامپوننت Board
رندر میشود اضافه میکنید:
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>
</>
);
}
حالا شما باید دوباره یک صفحه پر شده با شمارههای 1 تا 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>
</>
);
}
ساخت کامپوننت تعاملی
حال بیایید کامپوننت Square
را زمانی که روی آن کلیک میکنید با یک "X" پر کنیم. یک تابع به نام handleClick
درون Square
تعریف کنید. سپس onClick
را به props دکمه عنصر button که از Square
بازگشت داده میشود اضافه کنید:
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
اگر اکنون روی یک مربع کلیک کنید، باید یک لاگ در تب Console با متن "clicked!"
در پایین بخش مرورگر در CodeSandbox ببینید. هر بار که روی مربع کلیک کنید، "clicked!"
دوباره لاگ خواهد شد. لاگهای مکرر با همان پیام خط جدیدی در کنسول ایجاد نمیکنند. در عوض، شما یک شمارش در حال افزایش را در کنار اولین لاگ "clicked!"
خود خواهید دید.
اگر شما در حال دنبال کردن این آموزش با استفاده از محیط توسعه محلی خود هستید، نیاز دارید کنسول مرورگر خود را باز کنید. به عنوان مثال، اگر از مرورگر Chrome استفاده میکنید، میتوانید کنسول را با میانبر صفحهکلید Shift + Ctrl + J (در ویندوز/لینوکس) یا Option + ⌘ + J (در macOS) مشاهده کنید.
در مرحله بعد، شما میخواهید که کامپوننت Square "به خاطر بسپارد" که روی آن کلیک شده است و با یک علامت "X" پر شود. برای "به خاطر سپردن" وضعیت ها، کامپوننتها از state استفاده میکنند.
React یک تابع ویژه به نام useState
ارائه میدهد که میتوانید از کامپوننت خود آن را فراخوانی کنید تا امکان "به خاطر سپردن" داده ها به کامپوننت شما اضافه شود. بیایید مقدار جاری Square
را در state ذخیره کنیم و آن را هنگام کلیک شدن تغییر دهیم.
دستور ایمپورت useState
را در بالای فایل اضافه کنید. ویژگی value
را از کامپوننت Square
حذف کرده و به جای آن، یک خط جدید در ابتدای تمام Square
اضافه کنید که useState
را فراخوانی کند و یک متغیر state به نام value
را برگرداند:
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
متغیر value
مقدار را ذخیره میکند و setValue
تابعی است که میتواند برای تغییر مقدار استفاده شود. مقدار null
ارسال شده به useState
به عنوان مقدار اولیه برای این متغیر وضعیت استفاده میشود، بنابراین مقدار value
در ابتدا برابر با null
است.
از آنجا که کامپوننت Square
دیگر ورودی ای نمیپذیرد، ویژگی value
را باید از تمام نه کامپوننت Square که توسط کامپوننت 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>
</>
);
}
حالا باید کامپوننت Square
را تغییر دهید تا هنگام کلیک عبارت "X" را نمایش دهد. هندلر رویداد console.log("clicked!");
را با setValue('X');
جایگزین کنید. حالا کامپوننت Square
شما به این شکل خواهد بود:
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
با فراخوانی این تابع setValue
در هندلر onClick
، به React میگویید که کامپوننت Square
را هر بار که روی <button>
آن کلیک میشود، دوباره رندر کند. پس از بهروزرسانی، مقدار جدید value
در Square
برابر 'X'
خواهد بود، بنابراین شما "X" را بر روی تخته بازی مشاهده خواهید کرد. بر روی هر Square کلیک کنید، و "X" باید نمایش داده شود:

هر Square
دارای وضعیت مربوط به خود است: value
ذخیره شده در هر Square
به طور کاملاً مستقل از دیگران است. زمانی که شما یک تابع set
را در یک کامپوننت فراخوانی میکنید، React بهطور خودکار کامپوننتهای فرزند آن را نیز بهروزرسانی میکند.
پس از انجام تغییرات فوق، کد شما به این شکل خواهد بود:
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>
</>
);
}
ابزارهای توسعه React
ابزارهای توسعه React به شما امکان میدهند props و state کامپوننتهای React خود را در زمان اجرا بررسی کنید. شما میتوانید تب ابزارهای توسعه React را در پایین بخش مرورگر در CodeSandbox پیدا کنید:

برای بازرسی یک کامپوننت خاص در صفحه، از دکمه در گوشه بالا سمت چپ ابزارهای توسعه React استفاده کنید:

برای توسعه محلی، ابزارهای توسعه React به عنوان یک افزونه مرورگر Chrome، Firefox، و Edge در دسترس است. آن را نصب کنید و تب Components در ابزارهای توسعه مرورگر شما برای سایتهای استفاده کننده از React ظاهر خواهد شد.
تکمیل بازی
در این مرحله، شما تمام اجزای بنیادی برای بازی دوز را دارید. برای داشتن یک بازی کامل، اکنون نیاز دارید تا "X" و "O" را روی تخته به صورت متناوب قرار دهید و نیاز به روشی دارید تا برنده را تعیین کنید.
بالا بردن وضعیت
در حال حاضر، هر کامپوننت Square
(مربع) بخشی از وضعیت بازی را نگهداری میکند. برای بررسی برنده در بازی دوز، کامپوننت Board
(صفحه بازی) باید بهنوعی وضعیت هر یک از 9 کامپوننت Square
را بداند.
چگونه این موضوع را پیاده سازی می کنید؟ در ابتدا ممکن است فکر کنید که Board
باید از هر Square
را برای وضعیتش "سؤال" کند. با اینکه این رویکرد از نظر فنی در React ممکن است، ما آن را توصیه نمیکنیم زیرا کد برای درک دشوار و مستعد بروز باگ و سخت برای بازنویسی میشود. در عوض، بهترین رویکرد این است که وضعیت بازی را در کامپوننت والد Board
به جای هر Square
حفظ کنید. کامپوننت Board
میتواند با ارسال ورودی به هر Square
بگوید که چه چیزی نمایش دهد، مانند زمانی که شما یک عدد را به هر مربع ارسال کردید.
برای جمعآوری داده از چندین فرزند، یا برای برقراری ارتباط بین دو کامپوننت، وضعیت مشترک را به جای تعریف وضعیت در خود کامپوننت ها باید در کامپوننت والد آنها اعلام کنید. کامپوننت والد میتواند آن وضعیت را از طریق props به فرزندان بازگرداند. این کار فرزندان را با یکدیگر و همچنین با والدشان همگام نگه میدارد.
بالا بردن وضعیت به کامپوننت والد در زمانی که کامپوننتهای React ریفکتور میشوند رایج است.
بیایید از این فرصت استفاده کنیم و آن را امتحان کنیم. کامپوننت Board
را ویرایش کنید تا یک متغیر وضعیت به نام squares
داشته باشد که بهطور پیشفرض به آرایهای از 9 مقدار null که مربوط به 9 مربع است، مقدار دهی شود:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null)
آرایهای با نه عنصر ایجاد میکند و هر یک از آنها را به null
تنظیم میکند. فراخوانی تابع useState()
با این مقدار، یک متغیر وضعیت squares
با مقدار پیش فرض آرایه 9 آیتمی تعریف می کند. هر ورودی در آرایه مربوط به مقدار یک مربع است. وقتی بعداً صفحه بازی را پر کنیم، آرایه squares
به این صورت خواهد بود:
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
حالا کامپوننت Board
شما باید ویژگی value
مربوط به هر Square
را، برای رندر کردن به آن ارسال کند:
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>
</>
);
}
بعد، باید کامپوننت Square
را ویرایش کنید تا ویژگی value
را از مجددا از کامپوننت Board دریافت کند. این کار نیاز به حذف پیگیری وضعیت value
در خود کامپوننت Square و ویژگی onClick
دکمه دارد:
function Square({value}) {
return <button className="square">{value}</button>;
}
در این نقطه شما باید یک صفحه خالی دوز مجدد ببینید:

و کد شما باید به این صورت باشد:
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>
</>
);
}
هر Square
اکنون ویژگی value
را دریافت خواهد کرد که یا 'X'
، یا 'O'
یا null
برای وضعیت خالی خواهد بود.
حالا، برای هندل کردن کلیک بر روی یک Square
نیاز به ایجاد تغییراتی دارید تا اینکه با کلیک چه اتفاقی بیفتد را مشخص کنید. اکنون کامپوننت Board
وضعیت بازی را نگهداری میکند. شما باید روشی برای بهروزرسانی وضعیت Board
توسط Square
در نظر بگیرید. از آنجا که وضعیت مخصوص کامپوننتی است که آن را تعریف کرده ، برای همین نمیتوانید وضعیت Board
را مستقیماً از داخل Square
بهروزرسانی کنید.
در عوض، شما یک تابع از کامپوننت Board
به کامپوننت Square
ارسال میکنید و Square
آن تابع را در زمان کلیک روی مربع، فراخوانی میکند. شما با تابعی که کامپوننت Square
هنگام کلیک فراخوانی خواهد کرد، شروع خواهید کرد. آن تابع را onSquareClick
نامگذاری میکنید:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
سپس، تابع onSquareClick
را به props کامپوننت Square
اضافه کنید:
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
حالا ویژگی onSquareClick
کامپوننت مربع را باید به تابعی در کامپوننت Board
متصل کنید. برای این اتصال شما ویژگی onSquareClick
را به handleClick
به صورت زیر وصل کنید:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}
در نهایت، باید تابع handleClick
را درون کامپوننت Board
تعریف کنید تا آرایه squares
را بهروزرسانی کند:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
تابع handleClick
یک کپی از آرایه squares
به نامnextSquares
و با استفاده از متد slice()
جاوااسکریپت ایجاد میکند. سپس، handleClick
آرایه nextSquares
را بهروز میکند تا X
را به اولین مربع ([0]
index) اضافه کند.
فراخوانی تابع setSquares
به React اطلاع می دهد که وضعیت کامپوننت تغییر کرده است. این کار موجب رندر مجدد کامپوننتی که از وضعیت squares
استفاده میکنند (Board
) و همچنین فرزندان آن (کامپوننتهای Square
که صفحه بازی را تشکیل میدهند) میشود.
جاوااسکریپت از بستارها یا closures پشتیبانی میکند، به این معنی که یک تابع داخلی (مثلاً handleClick
) به متغیرها و توابعی که در یک تابع خارجی (مثلاً Board
) تعریف شدهاند، دسترسی دارد. تابع handleClick
میتواند وضعیت squares
را بخواند و متد setSquares
را فراخوانی کند، زیرا هر دو درون تابع Board
تعریف شدهاند.
حالا میتوانید X
ها را به صفحه بازی اضافه کنید... اما فقط به مربع بالای سمت چپ. چون تابع handleClick
شما فقط خانه با اندیس (0
) را بروز رسانی می کند. بیایید handleClick
را به شکلی تغییر دهیم که با کلیک روی هر مربع، وضعیت آن مربع را بهروزرسانی کند. برای این کار یک آرگومانی به تابع handleClick
اضافه کنید که اندیس مربع کلیک شده را برای بروزرسانی بگیرد:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
nextSquares[i] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
سپس، نیاز دارید که اندیس را در زمان کلیک به handleClick
ارسال کنید. شما میتوانید برای شروع ورودی onSquareClick
مربع را بهطور مستقیم به handleClick(0)
در JSX تنظیم کنید، اما این روش واقعن کار نمیکند:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
اما دلیل اینکه این روش کار نمیکند چیست؟ فراخوانی handleClick(0)
بخشی از رندر کامپوننت صفحه بازی خواهد بود. از آنجا که handleClick(0)
وضعیت کامپوننت صفحه بازی را با فراخوانی setSquares
تغییر میدهد، کامپوننت صفحه بازی شما مجدداً رندر خواهد شد. اما این بار handleClick(0)
دوباره اجرا میشود و منجر به یک حلقه بینهایت میشود و با خطای زیر در کنسول مواجه خواهید شد:
Too many re-renders. React limits the number of renders to prevent an infinite loop.
چرا این مشکل در مراحل قبلی رخ نداد؟
زمانی که onSquareClick={handleClick}
را نوشتید، تابع handleClick
را به عنوان ورودی منتقل میکردید. درواقع آن را فراخوانی نکردید! اما حالا آن تابع را بلافاصله فراخوانی میکنید--به پرانتزها در handleClick(0)
توجه کنید-- و نتیجه فراخوانی را بعنوان ورودی به کامپوننت پاس می دهید. اگر نمیخواهید handleClick
را تا زمانی که کاربر کلیک کند، فراخوانی کنید، نباید به این روش پیش بروید!
برای رفع مشکل میتوانید تابعی مانند handleFirstSquareClick
تعریف کنید تا handleClick(0)
را درون آن فراخوانی کنید، و تابعی مانند handleSecondSquareClick
که handleClick(1)
را فراخوانی میکند و ... . حال این توابع را بعنوان props می توانید مانند onSquareClick={handleFirstSquareClick}
به کامپوننت فرزند منتقل کنید. این کار مشکل حلقه بینهایت را حل خواهد کرد.
با این حال، تعریف نه تابع متفاوت و دادن نام به هر یک از آنها بسیار طولانی است. در عوض، بیایید این کار را انجام دهیم:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
به دستور جدید () =>
توجه کنید. در اینجا، () => handleClick(0)
یک تابع پیکانی (یا Arrow) است، که یک روش کوتاهتر برای تعریف توابع است. هنگامی که مربع کلیک میشود، کد بعد از =>
"پیکان" اجرا خواهد شد، و handleClick(0)
را فراخوانی میکند.
حالا شما نیاز دارید که سایر هشت مربع را بهروز کنید تا handleClick
را از توابع فلکی که منتقل میکنید، فراخوانی کنند. مطمئن شوید که آرگومان هر فراخوانی handleClick
با index مربع درست مطابقت داشته باشد:
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>
</>
);
};
حالا میتوانید دوباره X
ها را به هر مربع در صفحه بازی اضافه کنید با کلیک کردن بر روی آنها:

اما این بار تمام مدیریت وضعیت توسط کامپوننت Board
انجام میشود!
حال بعد از این تغییرات کد شما باید به این صورت باشد: مشاهده نتیجه کد در CodeSandBox
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>
</>
);
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
حالا که مدیریت وضعیت در کامپوننت Board
است، کامپوننت والد Board
ورودی ها را به کامپوننتهای فرزند Square
منتقل میکند تا آنها بهطور صحیح وضعیت را نمایش دهند. وقتی بر روی یک Square
کلیک میشود، کامپوننت فرزند Square
اکنون از کامپوننت والد Board
درخواست میکند که وضعیت صفحه بازی را بهروزرسانی کند. هنگامی که وضعیت Board
تغییر کرد، هم کامپوننت Board
و هم هر Square
فرزند بهطور خودکار مجدداً رندر میشوند. نگهداری وضعیت تمام مربعها در کامپوننت Board
به آن اجازه میدهد تا برنده را در آینده تعیین کند.
بیایید خلاصه کنیم که چه اتفاقی میافتد زمانی که کاربر بر روی مربع بالای سمت چپ صفحهی شما کلیک میکند تا یک X
به آن اضافه کند:
- کلیک بر روی مربع بالای سمت چپ تابعی را اجرا میکند که
button
به عنوان ویژگیonClick
ازSquare
دریافت کرده است. کامپوننتSquare
آن تابع را به عنوان ورودیonSquareClick
ازBoard
دریافت کرده است. کامپوننتBoard
آن تابع را بهطور مستقیم در JSX تعریف کرده است. این تابعhandleClick
را با آرگومان0
فراخوانی میکند. handleClick
از آرگومان (0
) برای بهروزرسانی اولین عنصر آرایهsquares
ازnull
بهX
استفاده میکند.- وضعیت
squares
کامپوننتBoard
بهروزرسانی شد، بنابراینBoard
و تمام فرزندان آن مجدداً رندر میشوند. این باعث میشود که ورودیvalue
کامپوننتSquare
با اندیس0
ازnull
بهX
تغییر کند.
در نهایت کاربر میبیند که مربع بالای سمت چپ از خالی پس از کلیک به مقدار X
تبدیل شده است.
ویژگی onClick
عنصر <button>
برای React معنای خاصی دارد زیرا این یک کامپوننت درونی است. برای کامپوننتهای سفارشی مانند Square
، نامگذاری به شما بستگی دارد. شما میتوانید هر نامی به ویژگی onSquareClick
کامپوننت Square
یا تابع handleClick
کامپوننت Board
بدهید و کد به همان صورت کار خواهد کرد. در React، معمولاً برای props که نمایانگر رویدادها هستند، از نامهای onSomething
و برای تعریف توابعی که آن رویدادها را مدیریت میکنند، از handleSomething
استفاده میشود.
چرا تغییر ناپذیری مهم است
توجه کنید که در handleClick
، به جای اینکه آرایه موجود را تغییر دهید با فراخوانی .slice()
یک کپی از آرایه squares
ایجاد میکنید. در این قسمت درباره اینکه چرا ما به تغییر ناپذیری نیاز داریم و اینکه چرا عدم تغییر پذیری مهم است بحث می کنیم.
به طور کلی، دو رویکرد برای تغییر داده وجود دارد. اولین رویکرد تغییر دادهها است با تغییر مستقیم مقادیر داده. رویکرد دوم جایگزینی داده با یک کپی جدید از داده است که تغییرات مورد نظر را دارد. اگر آرایه squares
را مستقیم تغییر دهید نتیجه به این صورت خواهد بود:
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];
و حال تغییر داده بدون تغییر منبع اصلی که همان squares
است به این شکل خواهد بود:
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`
نتیجه یکسان است اما عدم تغییر (تغییر مستقیم دادههای زیرین) چندین مزیت بهمراه دارد.
تغییر ناپذیری، پیاده سازی ویژگیهای پیچیده را بسیار راحتتر میکند. در ادامه این آموزش، شما ویژگی "سفر در زمان" را پیادهسازی خواهید کرد که به شما اجازه میدهد تاریخچه بازی را مشاهده کرده و به حرکات گذشته "برگردید". این عملکرد ویژه فقط مربوط به بازیها نیست – قابلیت بازگشت و پیشروی برخی عملها یکی از نیازهای متداول برای بعضی از برنامهها است. جلوگیری از تغییر مستقیم داده به شما اجازه میدهد نسخههای قبلی داده را نگهداری کرده و در آینده از آنها استفاده کنید.
از دیگر مزیتهای عدم تغییر پذیری این است که به طور پیشفرض، تمام کامپوننتهای فرزند زمانی که وضعیت کامپوننت والد تغییر میکند، بهطور خودکار مجدداً رندر میشوند. این رندر مجدد حتی شامل کامپوننتهایی که متاثر از تغییر قرار نگرفتهاند نیز می شود. اگرچه رندر مجدد به خودی خود برای کاربر ملموس نیست (شما نباید بهطور فعال سعی کنید از آن جلوگیری کنید!)، گاهی ممکن است به خاطر بهبود عملکرد از رندر مجدد بخشی از درخت نمایش که مستقیمن تحت تاثیر تغییر قرار نمیگیرد جلوگیری کنید. عدم تغییر پذیری مقایسه اینکه آیا دادههای آن بخش تغییر کرده است یا نه را برای کامپوننتها بسیار کم هزینه تر میکند. برای درک بهتر از این کار و اینکه React چگونه و چه زمانی تصمیم به رندر مجدد کامپوننت میگیرد در مراجع API memo
بیشتر بخوانید.
بازی نوبتی
حال زمان آن رسیده است که یک نقص اساسی در این بازی را برطرف کنید: "O" ها بر روی صفحه نشان داده نمی شوند!
شما اولین حرکت را بهطور پیشفرض "X" قرار داده اید. بیایید این را با اضافه کردن یک وضعیت دیگر به کامپوننت Board حل کنیم:
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
هر بار که یک بازیکن حرکت میکند، xIsNext
(یک بولین) تغییر خواهد کرد تا تعیین کند که کدام بازیکن، بازیکن بعدی است. شما باید تابع handleClick
کامپوننت Board
را تغییر دهید تا مقدار 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 (
//...
);
}
حال وقتی بر روی مربعهای مختلف کلیک میکنید، آنها باید بین X
و O
نوبت بزنند!
اما صبر کنید، یک مشکل وجود دارد. سعی کنید چندین بار بر روی یک مربع کلیک کنید:

X
با یک O
جایگزین میشود!
زمانی که یک مربع را با X
یا O
علامتگذاری میکنید، بررسی نمی کنید که آیا مربع قبلاً مقدار X
یا O
را دارد یا نه. برای حل این مشکل میتوانید در تابع بازگشت زودهنگام انجام دهید. اول باید بررسی کنید که آیا مربع قبلاً دارای X
یا O
است یا خیر. اگر مربع قبلاً پر شده باشد، باید در تابع handleClick
قبل از تغییر وضعیت دستور return
را اجرا کنید.
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
حالا میتوانید فقط X
یا O
را به مربعهای خالی اضافه کنید! این چیزی است که کد شما در این مرحله باید به نظر برسد:
مشاهده کد تا اینجا در CodeSandBox
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>
</>
);
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
اعلام برنده
حالا که بازیکنان میتوانند نوبتی بازی کنند، باید نشان دهید که چه کسی برنده بازی شده و دیگر نوبتی برای انجام وجود ندارد. برای اینکار یک تابع کمکی به نام calculateWinner
تعریف کنید که یک آرایه از 9 مربع را بهعنوان ورودی میگیرد و بررسی میکند که آیا برندهای وجود دارد یا نه و سپس برنده را با برگرداندن 'X'
، 'O'
یا null
مشخص می کند.
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;
}
مهم نیست که تابع calculateWinner
را قبل یا بعد از Board
تعریف کنید. بیایید آن را در انتها قرار دهیم تا هر بار که کامپوننتهای خود را ویرایش میکنید، نیازی به اسکرول کردن نداشته باشید.
تابع calculateWinner(squares)
را در تابع handleClick
کامپوننت Board
برای بررسی اینکه آیا یک بازیکن برنده شده است، فراخوانی کنید. این بررسی را می توانید همزمان با بررسی خالی بودن خانه، انجام دهید. ما میخواهیم در هر دو حالت زودتر از مورد اجرا را تمام کنیم:
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
برای اینکه بازیکنان بدانند که چه زمانی بازی تمام میشود، میتوانید متنی مانند "برنده: X" یا "برنده: O" را نمایش دهید. برای اینکار یک بخش status
به کامپوننت Board
اضافه کنید. اگر بازی به پایان رسیده باشد وضعیت این بخش برنده را مشخص می کنید و اگر بازی در حال انجام باشد، نوبت بازیکن بعدی را نشان خواهد داد:
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">
// ...
)
}
تبریک میگوییم! اکنون یک بازی دوز واقعی درست کرده اید. و شما همچنین اصول React را یاد گرفتهاید. بنابراین شما برنده واقعی هستید. کد شما اکنون باید به صورت زیر باشد: مشاهده کد بازی تا اینجا
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;
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
اضافه کردن سفر در زمان
به عنوان تمرین نهایی، بیایید امکان "بازگشت به زمان" یا بازگشت به حرکات قبلی را به بازی را اضافه کنیم.
نگهداری تاریخچه حرکات
اگر آرایه squares
را تغییر دهید، پیادهسازی سفر در زمان بسیار دشوار می شد.
با این حال، از متد slice()
برای ایجاد یک کپی جدید از آرایه squares
پس از هر حرکت استفاده کردهاید و وضعیت را مستقیم تغییر نداده اید. این به شما اجازه میدهد تا هر نسخه قبلی از آرایه squares
را ذخیره کره و بین نوبتهایی که قبلاً اتفاق افتادهاند، جابجا شوید.
آرایههای squares
قبلی را در یک آرایه دیگر به نام history
ذخیره کنید و آن را بهعنوان یک متغیر وضعیت جدید ذخیره کنید. آرایه history
نمایانگر تمام وضعیتهای صفحه بازی، از اولین تا آخرین حرکت است و شکلی شبیه به این دارد:
[
// 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'],
// ...
]
بالا بردن دوباره وضعیت
اکنون یک کامپوننت سطح بالا به نام Game
ایجاد کنید تا لیستی از حرکات گذشته را نمایش دهد. اینجا جایی است که وضعیت history
را که شامل کل تاریخچه بازی است، قرار خواهید داد.
قرار دادن وضعیت history
در کامپوننت Game
به شما امکان میدهد تا وضعیت squares
را از کامپوننت فرزند آن یعنی Board
حذف کنید. همانطور که قبلاً وضعیت را از کامپوننت Square
به Board
منتقل کردید، اکنون وضعیت را از Board
به کامپوننت سطح بالای Game
منتقل خواهید کرد. این کار کنترل کامل دادههای Board
را به Game
میدهد و اجازه میدهد که Board
را با استفاده از تاریخچه حرکات گذشته دوباره رندر کند.
ابتدا، یک کامپوننت Game
با export default
اضافه کنید. این کامپوننت باید کامپوننت Board
و مقداری مارکاپ را رندر کند:
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>
);
}
توجه داشته باشید که باید دستور export default
را از قبل از تعریف function Board() {
حذف کرده و قبل از function Game() {
اضافه کنید. این کار به فایل index.js
میگوید که از Game
به عنوان کامپوننت سطح بالا به جای Board
استفاده کند. div
های اضافی که توسط Game
بازگردانده میشوند، فضایی برای اطلاعات بازی که بعداً به صفحه اضافه میکنید ایجاد میکنند.
به کامپوننت Game
باید وضعیتهای جدید اضافه کنید تا مشخص کند که نوبت بازیکن بعدی کیست و تاریخچه حرکات را ذخیره کند:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...
توجه کنید که [Array(9).fill(null)]
آرایهای با یک عنصر است که خود شامل آرایهای از ۹ مقدار null
است.
برای رندر کردن خانههای مربوط به حرکت فعلی، باید آخرین آیتم آرایه history
را بخوانید. نیازی به useState
نیز ندارید، زیرا از قبل اطلاعات کافی برای محاسبه آن هنگام رندر دارید:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
// ...
سپس، یک تابع handlePlay
درون کامپوننت Game
ایجاد کنید که توسط Board
برای بهروزرسانی بازی فراخوانی شود. xIsNext
، currentSquares
و handlePlay
را به عنوان props
به Board
ارسال کنید:
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} />
//...
)
}
اکنون بیایید کامپوننت Board
را به کامپوننتی کاملاً کنترلشده توسط props
تبدیل کنیم. کامپوننت Board
را تغییر دهید تا سه prop
جدید دریافت کند: xIsNext
، squares
، و یک تابع جدید به نام onPlay
که Board
هنگام انجام حرکت بازیکن آن را فراخوانی میکند. سپس دو خط اول تابع Board
که useState
را فراخوانی میکنند، حذف کنید:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
اکنون در handleClick
در Board
، به جای setSquares
و setXIsNext
از فراخوان تابع جدید 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);
}
//...
}
کامپوننت Board
بهطور کامل توسط props
هایی که از کامپوننت Game
دریافت میکند، کنترل میشود. شما باید تابع handlePlay
را در کامپوننت Game
پیادهسازی کنید تا بازی مجدداً کار کند.
تابع handlePlay
هنگام فراخوانی باید چه کاری انجام دهد؟ به یاد داشته باشید که Board
قبلاً setSquares
را با یک آرایهی بهروزشده فراخوانی میکرد؛ اما اکنون آرایهی squares
بهروزشده را به onPlay
ارسال میکند.
تابع handlePlay
باید وضعیت (state) کامپوننت Game
را بهروزرسانی کند تا یک رندر مجدد انجام شود، اما دیگر تابع setSquares
برای فراخوانی در دسترس نیست—شما اکنون از متغیر history
برای ذخیرهی این اطلاعات استفاده میکنید. باید history
را با افزودن آرایهی جدید squares
بهروزرسانی کنید. همچنین باید مقدار xIsNext
را تغییر دهید، همانطور که Board
قبلاً این کار را انجام میداد.
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
اینجا، [...history, nextSquares]
یک آرایهی جدید ایجاد میکند که شامل تمام آیتمهای history
است و سپس nextSquares
را به آن اضافه میکند. (میتوانید ...history
را بهعنوان "enumerate all the items in history
" یا "تمام آیتمهای history
را لیست کن" در نظر بگیرید.)
برای مثال، اگر مقدار history
برابر [[null,null,null], ["X",null,null]]
و مقدار nextSquares
برابر ["X",null,"O"]
باشد، آرایهی جدید [...history, nextSquares]
به این صورت خواهد بود: [[null,null,null], ["X",null,null], ["X",null,"O"]]
.
در این مرحله، وضعیت (state) به کامپوننت Game
منتقل شده است و رابط کاربری (UI) باید بهطور کامل مانند قبل از بازسازی، کار کند. در اینجا کد موردنظر در این مرحله نمایش داده شده است:
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;
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
نمایش حرکات گذشته
از آنجا که در حال ضبط تاریخچه بازی هستید، اکنون میتوانید لیستی از حرکات گذشته را به بازیکن نمایش دهید.
المانهای ریاکت مانند <button>
اشیای جاوااسکریپتی عادی هستند؛ میتوانید آنها را در برنامه خود جابجا کنید. برای رندر کردن چندین آیتم در ریاکت، میتوانید از یک آرایه از المانهای ریاکت استفاده کنید.
از آنجایی که از قبل آرایهای از حرکات history
را در وضعیت دارید، اکنون باید آن را به یک آرایه از المانهای ریاکت تبدیل کنید. در جاوااسکریپت، برای تبدیل یک آرایه به آرایه دیگر، میتوانید از متد map
استفاده کنید:
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
شما از map
برای تبدیل history
به دکمههایی که امکان پرش به حرکات گذشته را فراهم میکنند استفاده خواهید کرد:
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>
);
}
الان می توانید نتیجه کار را در CodeSandBox ببینید. اما توجه داشته باشید که اخطار زیر را نیز در کنسول مشاهده خواهید کرد که در بخش بعدی این مشکل را برطرف می کنید:
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Game`.
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;
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
هنگامی که درون تابعی که به map
ارسال شده است، آرایهی history
را پیمایش میکنید، آرگومان squares
به ترتیب از هر عنصر history
عبور میکند و آرگومان move
مقدار ایندکس هر آرایه را نشان میدهد: 0
، 1
، 2
، …. (در بیشتر موارد، شما به خود عناصر آرایه نیاز دارید، اما برای نمایش لیستی از حرکات، فقط به ایندکسها نیاز خواهید داشت.)
برای هر حرکت در تاریخچهی بازی دوز (tic-tac-toe)، یک آیتم لیست <li>
ایجاد میکنید که شامل یک دکمه <button>
است. این دکمه دارای یک هندلر onClick
است که تابعی به نام jumpTo
را فراخوانی میکند (که هنوز آن را پیادهسازی نکردهاید).
در حال حاضر، باید لیستی از حرکاتی که در بازی رخ دادهاند را مشاهده کنید و یک خطا در کنسول ابزارهای توسعهدهنده (developer tools console) ببینید. بیایید بررسی کنیم که خطای "کلید" (key error) به چه معناست.
انتخاب کلید
هنگامی که لیست را رندر میکنید، ریاکت اطلاعاتی درباره هر آیتم ذخیره میکند. هنگام بهروزرسانی لیست، ریاکت باید تشخیص دهد که چه چیزی تغییر کرده است. شما ممکن است آیتمهایی را اضافه، حذف، جابهجا یا بهروزرسانی کنید.
برای حل این مشکل، باید یک ویژگی کلید برای هر آیتم لیست مشخص کنید که آن را از سایر آیتمها متمایز کند مثل گذار از:
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
به
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
علاوه بر اعداد بهروز شده، اگر یک انسان این تغییرات را بخواند، احتمالاً متوجه خواهد شد که ترتیب "Alexa" و "Ben" جابهجا شده و "Claudia" بین آنها اضافه شده است. با این حال، React یک برنامهی کامپیوتری است و از قصد شما آگاهی ندارد، بنابراین شما باید یک ویژگی key برای هر آیتم لیست مشخص کنید تا React بتواند هر آیتم را از بقیه تشخیص دهد. اگر دادههای شما از یک پایگاه داده میآمدند، میتوانستید از شناسههای پایگاه دادهی "Alexa"، "Ben" و "Claudia" بهعنوان کلیدها استفاده کنید.
<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>
وقتی یک لیست مجدداً رندر میشود، ریاکت کلید هر آیتم لیست را گرفته و در میان آیتمهای لیست قبلی به دنبال کلید مشابه میگردد. اگر لیست فعلی حاوی کلیدی باشد که قبلاً وجود نداشته، ری اکت یک کامپوننت جدید میسازد. اگر لیست فعلی فاقد کلیدی باشد که در لیست قبلی وجود داشت، ری اکت کامپوننت قبلی را از بین میبرد. اگر دو کلید با هم مطابقت کنند، کامپوننت مربوطه جابه جا میشود.
کلیدها به ریاکت در مورد هویت هر کامپوننت اطلاع میدهند و این امکان را فراهم میکنند که ریاکت بتواند state را بین رندرهای مجدد حفظ کند. اگر کلید یک کامپوننت تغییر کند، آن کامپوننت از بین رفته و با state جدید مجدداً ساخته میشود.
ویژگی key
یک ویژگی خاص و رزرو شده در ریاکت است. هنگامی که یک المنت ساخته میشود، ریاکت ویژگی key
را استخراج کرده و آن را مستقیماً روی عنصر بازگشتی ذخیره میکند. حتی اگر به نظر برسد key
به عنوان ورودی ارسال شده، ریاکت به صورت خودکار از key
برای تصمیم گیری درباره بهروزرسانی کامپوننتها استفاده میکند. هیچ راهی برای کامپوننت وجود ندارد تا از کلید اختصاص داده شده توسط والد به خود مطلع شود.
توصیه اکید میشود که هنگام ساخت لیستهای پویا حتماً کلیدهای مناسب اختصاص دهید. اگر کلید مناسب ندارید، بهتر است ساختار داده های خود را تغییر دهید تا کلید مناسب داشته باشید.
اگر هیچ کلیدی مشخص نشود، ریاکت خطایی گزارش داده و به طور پیشفرض از ایندکس آرایه به عنوان کلید استفاده میکند. استفاده از ایندکس آرایه به عنوان کلید هنگام جابهجایی آیتمهای لیست یا افزودن/حذف آیتمها مشکل زا است. ارسال صریح key={i}
خطا را از بین می برد اما همان مشکلات اندیس آرایه را داشته و در بیشتر موارد توصیه نمیشود.
کلیدها نیازی به منحصر به فرد بودن در سطح کل برنامه ندارند؛ فقط باید بین کامپوننتها و خواهرهایشان منحصر به فرد باشند.
پیادهسازی قابلیت سفر در زمان
در تاریخچه بازی، هر حرکت یک شناسه منحصربهفرد دارد: عدد ترتیبی حرکت. از آنجایی که حرکات هرگز مرتب نمیشوند یا حذف نمیشوند، میتوان از اندیس حرکت به عنوان key
استفاده کرد.
در تابع Game
، کلید را می توانید به صورت <li key={move}>
اضافه کنید و با ریلود برنامه، دیگر خبری از خطا نخواهد بود:
const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
مشاهده کدهای برنامه تا اینجا در CodeSandBox
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;
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
قبل از اینکه بتوانید jumpTo
را پیادهسازی کنید، نیاز دارید کامپوننت Game
وضعیت فعلی مرحلهای که کاربر در حال مشاهده آن است را نگهداری کند. برای این کار یک متغیر وضعیت جدید به نام currentMove
تعریف کنید که پیشفرض آن 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];
//...
}
سپس تابع jumpTo
تعریف شده درون Game
را بهروزرسانی کنید تا currentMove
را تغییر دهد. همچنین اگر عددی که currentMove
به آن تغییر میکند زوج بود xIsNext
را با true
ست کنید.
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
حالا دو تغییر در تابع handlePlay
کامپوننت Game
باید ایجاد کنید تا با کلیک روی مربعها اجرا شوند:
- اگر "به گذشته برگردید" و سپس یک حرکت جدید از آن نقطه انجام دهید، فقط باید تاریخچه را تا آن نقطه نگه دارید. به جای اضافه کردن
nextSquares
بهhistory
، بعد از همه آیتمها (با سینتکس spread...
)، آن را باید بعد از همه آیتمهایhistory.slice(0, currentMove + 1)
اضافه میکنید تا فقط بخش مربوطه از تاریخچه قدیمی حفظ شود. - هر بار که حرکتی انجام میشود، باید
currentMove
را به آخرین ورودی تاریخچه بهروزرسانی کنید.
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
در نهایت کامپوننت Game
را اصلاح میکنید تا حرکت انتخاب شده فعلی را به جای نمایش آخرین حرکت، نمایش:
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];
// ...
}
اگر روی هر مرحله در تاریخچه بازی کلیک کنید، صفحه دوز باید بلافاصله به حالتی بهروزرسانی شود که پس از آن مرحله وجود داشت.
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;
}
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
تمیزکاری نهایی
اگر کد را دقیق بررسی کنید، متوجه خواهید شد که xIsNext === true
زمانی رخ می دهد که currentMove
زوج است و xIsNext === false
زمانی رخ می دهد که currentMove
فرد است. بنابراین، میتوان مقدار xIsNext
را از مقدار 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);
}
// ...
}
دیگر نیازی به تعریف وضعیت xIsNext
و بروزرسانی آن نیست. الان دیگر امکان عدم همگامی این وضعیت با currentMove
وجود ندارد، حتی اگر در زمان کد نویسی اشتباهی مرتکب شوید.
جمعبندی
تبریک میگوییم! شما یک بازی دوز ساختید که:
- امکان انجام بازی دوز را فراهم میکند،
- مشخص میکند که چه زمانی یک بازیکن برنده شده است،
- تاریخچه بازی را ذخیره میکند،
- به بازیکنان اجازه میدهد تاریخچه بازی را بررسی کنند و حرکات قبلی را مشاهده کنند.
آفرین! امیدواریم که اکنون درک بهتری از نحوه کار ریاکت داشته باشید.
نتیجه نهایی را اینجا مشاهده کنید
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;
}
اگر زمان بیشتری دارید، ایدههای زیر را برای بهبود بازی امتحان کنید:
- برای حرکت جاری، به جای یک دکمه، پیام "شما در حرکت #... هستید" نمایش دهید.
Board
را تغییر دهید تا به جای کدگذاری مستقیم، از دو حلقه برای ساختن خانهها استفاده کند.- یک دکمه اضافه کنید که امکان تغییر ترتیب نمایش حرکات را فراهم کند.
- هنگام برد، سه خانهای که باعث برد شدهاند را برجسته کنید (و در صورت تساوی، پیغام مربوطه را نمایش دهید).
- موقعیت هر حرکت را به صورت
(سطر، ستون)
در لیست تاریخچه نمایش دهید.
در طول این آموزش، با مفاهیم ریاکت از جمله المانها، کامپوننتها، props
و وضعیت آشنا شدید. حالا که دیدید این مفاهیم چگونه برای ساخت یک بازی استفاده میشوند، مقاله تفکر در ریاکت را بررسی کنید تا ببینید چگونه این مفاهیم برای ساخت UI یک اپلیکیشن به کار گرفته میشوند.