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

mohsen1 ماه قبل4 هفته قبل
ارسال شده در
react/docs/v19

در این آموزش شما یک بازی دوز کوچک می‌سازید. در این آموزش فرض بر این است که شما هیچ دانش قبلی در مورد 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>;
}
    

همچنین می‌توانید با استفاده از کامپیوتر خود کدهای این آموزش را اجرا کنید. برای این کار باید:

  1. Node.js را نصب کنید.
  2. در تب CodeSandbox که قبلاً باز کرده‌اید، روی دکمه گوشه بالا-چپ کلیک کنید تا منو باز شود، و سپس Download Sandbox را انتخاب کنید و فایل آرشیو کدها را بروی سیستم خود دانلود کنید.
  3. آرشیو را از حالت فشرده خارج کنید، سپس یک ترمینال باز کرده و به دایرکتوری که از حالت فشرده خارج کرده‌اید بروید.
  4. وابستگی‌ها را با دستور npm install نصب کنید.
  5. با اجرای npm start یک سرور محلی اجرا می شود و با باز کردن آدرسی که به شما نمایش داده می شود، کد را در مرورگر خود اجرا کنید.

اگر جایی در مراحل بالا گیر کردید، نگذارید این موضوع شما را متوقف کند! به صورت آنلاین ادامه دهید و بعداً دوباره سعی کنید تا محیط توسعه محلی خود را راه‌اندازی کنید.

مروری بر کلیات

حالا که آماده شروع هستید، بیایید یک مروری بر React داشته باشیم!

بررسی کد ابتدایی

در CodeSandbox، شما سه بخش اصلی خواهید دید:

CodeSandbox با کد شروع
  1. بخش فایل‌ها که شامل لیستی از فایل‌ها مانند App.js، index.js، styles.css و یک پوشه به نام public است.
  2. ویرایشگر کد که در آن کد فایل انتخابی را مشاهده خواهید کرد.
  3. بخش مرورگر که در آن نتیجه کدی که نوشته‌اید نمایش داده می‌شود.

اگر فایل App.js در بخش فایل‌ها انتخاب شده باشد. محتوای آن فایل در ویرایشگر کد به صورت زیر نمایش داده می شود:

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

در بخش مرورگر باید یک مربع با علامت X نمایش داده شود، مانند این:

خانه های پر شده با 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>
    </>
  );
}
    

حالا خروجی باید این گونه باشد:

دو مربع پر شده با X

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

نه مربع پر شده با X در یک خط

اوه نه! مربع‌ها همه در یک خط هستند، نه در یک شبکه که ما برای برد به آن نیاز داریم. برای حل این مشکل باید مربع‌ها را با 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‌های استایل‌دار گروه‌بندی کرده‌اید، شما برد دوز خود را دارید:

tic-tac-toe board filled with numbers 1 through 9

اما الان یک مشکل دارید. نام کامپوننت شما 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 را از کامپوننت خود رندر کنید، نه کلمه "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 را ببینید:

tic-tac-toe board filled with numbers 1 through 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" باید نمایش داده شود:

پر کردن صفحه با 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 DevTools in CodeSandbox

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

Selecting components on the page with React DevTools

برای توسعه محلی، ابزارهای توسعه 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 ها را به هر مربع در صفحه بازی اضافه کنید با کلیک کردن بر روی آن‌ها:

پر کردن صفحه با 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 به آن اضافه کند:

  1. کلیک بر روی مربع بالای سمت چپ تابعی را اجرا می‌کند که button به عنوان ویژگی onClick از Square دریافت کرده است. کامپوننت Square آن تابع را به عنوان ورودی onSquareClick از Board دریافت کرده است. کامپوننت Board آن تابع را به‌طور مستقیم در JSX تعریف کرده است. این تابع handleClick را با آرگومان 0 فراخوانی می‌کند.
  2. handleClick از آرگومان (0) برای به‌روزرسانی اولین عنصر آرایه squares از null به X استفاده می‌کند.
  3. وضعیت 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 نوبت بزنند!

اما صبر کنید، یک مشکل وجود دارد. سعی کنید چندین بار بر روی یک مربع کلیک کنید:

O جایگزین X می‌شود

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 &#96;Game&#96;.
    
      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;
}
    

اگر زمان بیشتری دارید، ایده‌های زیر را برای بهبود بازی امتحان کنید:

  1. برای حرکت جاری، به جای یک دکمه، پیام "شما در حرکت #... هستید" نمایش دهید.
  2. Board را تغییر دهید تا به جای کدگذاری مستقیم، از دو حلقه برای ساختن خانه‌ها استفاده کند.
  3. یک دکمه اضافه کنید که امکان تغییر ترتیب نمایش حرکات را فراهم کند.
  4. هنگام برد، سه خانه‌ای که باعث برد شده‌اند را برجسته کنید (و در صورت تساوی، پیغام مربوطه را نمایش دهید).
  5. موقعیت هر حرکت را به صورت (سطر، ستون) در لیست تاریخچه نمایش دهید.

در طول این آموزش، با مفاهیم ری‌اکت از جمله المان‌ها، کامپوننت‌ها، props و وضعیت آشنا شدید. حالا که دیدید این مفاهیم چگونه برای ساخت یک بازی استفاده می‌شوند، مقاله تفکر در ری‌اکت را بررسی کنید تا ببینید چگونه این مفاهیم برای ساخت UI یک اپلیکیشن به کار گرفته می‌شوند.

رای
0
ارسال نظر
مرتب سازی:
اولین نفری باشید که نظر می دهید!