شرح تطبيقي لتنفيذ لعبة تيك تاك توو X-O
سنبني في هذا الدرس لعبة تيك تاك تو (X-O) صغيرة. لا يَفتَرِض هذا الدرس أي معرفة سابقة بـ React. التقنيات التي ستتعلمها في هذا البرنامج أساسية لبناء أي تطبيق React، وفهمها بشكل كامل سيمنحك فهمًا عميقًا لـ React.
هذا الدرس مقسم إلى عدة أقسام:
- التجهيز للدرس: سيعطيك نقطة انطلاق لمتابعة الدرس.
- نظرة عامة: سيعلمك أساسيات React؛ المكونات، الخصائص، والحالة.
- إكمال اللعبة: سيعلمك أكثر التقنيات شيوعًا في تطوير React.
- إضافة السفر عبر الزمن: سيعطيك فهمًا أعمق لقوة React الفريدة.
ماذا ستبني؟
في هذا الشرح التطبيقي، ستبني لعبة تيك تاك تو (X-O) تفاعلية باستخدام 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; } else { status = 'اللاعب التالي: ' + (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 = 'انتقل إلى الخطوة #' + move; } else { description = 'انتقل إلى بداية اللعبة'; } 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 وبناء الكود فيها.
نوصيك بتجربة لعبة X-O أعلاه قبل الاستمرار في الشرح التطبيقي. أحد الميزات التي ستلاحظها هي أن هناك قائمة مرقمة على يمين لوحة اللعبة. تعطيك هذه القائمة تاريخ جميع الحركات التي حدثت في اللعبة، ويتم تحديثها مع تقدم اللعبة.
بمجرد أن تنهى لعبة X-O، استمر في التمرير. ستبدأ بقالب أبسط في هذا الشرح التطبيقي. خطوتنا التالية هي إعدادك حتى تتمكن من بدء بناء اللعبة.
التجهيز للدرس
في محرر الكود المباشر أدناه، انقر فوق Fork في الزاوية اليمنى العليا لفتح المحرر في علامة تبويب جديدة باستخدام موقع CodeSandbox. يتيح لك CodeSandbox كتابة الكود في المتصفح ومعاينة كيف سيرى مستخدموك التطبيق الذي قمت بإنشائه. يجب أن تعرض علامة التبويب الجديدة مربعًا فارغًا وكود البداية لهذا الدرس.
export default function Square() { return <button className="square">X</button>; }
نظرة عامة
الآن أنت مستعد للبدء، دعنا نلقي نظرة عامة على 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
ينشئ مكونًا (Component). في React، المكون هو جزء من الكود قابل لإعادة الاستخدام يقدم جزءًا من واجهة المستخدم. المكونات تستخدم لعرض وإدارة وتحديث عناصر واجهة المستخدم في تطبيقك. دعنا نلقي نظرة على المكون سطرًا بسطر لنرى ما يحدث:
export default function Square() {
return <button className="square">X</button>;
}
السطر الأول يعرّف دالة اسمها Square
. الكلمة الأساسية export
في JavaScript تجعل هذه الوظيفة متاحة خارج هذا الملف. الكلمة الأساسية default
تخبر الملفات الأخرى التي تستخدم كودك أنها الوظيفة الرئيسية في ملفك.
export default function Square() {
return <button className="square">X</button>;
}
السطر الثاني يرجع زرًّا. الكلمة الأساسية return
في JavaScript تعني أن أي شيء يأتي بعدها يعاد كقيمة لمن يستدعي الوظيفة. <button>
هو عنصر JSX. عنصر JSX هو مزيج من كود JavaScript وعناصر HTML تصف ما تريد عرضه. className="square"
هو خاصية أو prop للزرّ تخبر CSS كيفية تنسيق الزر. X
هو النص المعروض داخل الزر. و </button>
يغلق عنصر JSX ليشير إلى أن أي محتوى يليه لا ينبغي وضعه داخل الزرّ.
styles.css
اضغط على الملف المسمى styles.css
في قسم الملفات في CodeSandbox. يحدد هذا الملف الأنماط لتطبيق 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';
الأسطر من 1 إلى 5 تستدعي كل القطع اللازمة معًا:
- 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>;
}
ستحصل على هذا الخطأ:
<>...</>
?تحتاج مكونات 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() {
//...
}
في هذه النقطة يجب أن يبدو الكود الخاص بك مثل هذا:
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
لتقديم مكون Square
باستخدام بناء جملة JSX:
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
لاحظ كيف تبدأ المكونات الخاصة بك Board
و Square
بحرف كبير على عكس عناصر div
في المتصفح.
دعنا نلقي نظرة:
أوه لا! لقد فقدت المربعات المرقمة التي كانت لديك من قبل. الآن يقول كل مربع “1”. لإصلاح هذا، ستستخدم الخصائص (props) لتمرير القيمة التي يجب أن يكون لكل مربع من المكون الأصلي (Board
) إلى مكونه الابن (Square
).
عدّل مكون Square
لقراءة خاصية value
التي ستمررها من Board
:
function Square({ value }) {
return <button className="square">1</button>;
}
تشير function Square({ value })
إلى أن مكون Square يمكن أن يُمرر إليه خاصية تسمى value
.
الآن تريد عرض هذه القيمة بدلاً من 1
داخل كل مربع. حاول القيام بذلك بهذه الطريقة:
function Square({ value }) {
return <button className="square">value</button>;
}
أوبس! هذا ليس ما تريده:
لقد أردت عرض متغير JavaScript يسمى value
من مكونك، وليس كلمة “value”. لـ”التخطي إلى JavaScript” من JSX، تحتاج إلى الأقواس المنحنية (curly braces). أضف الأقواس المنحنية حول value
في JSX على النحو التالي:
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>
</>
);
}
الآن يجب أن ترى شبكة من الأرقام مرة أخرى:
كودك المعدل يجب أن يبدو كالتالي:
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
عند النقر عليه. أعلن عن دالة (Function) تسمى handleClick
داخل Square
. ثم، أضف onClick
إلى خصائص عنصر JSX الزر المُرجع من Square
:
function Square({ value }) {
function handleClick() {
console.log('ضُغطت!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
إذا ضغطت على مربع الآن، يجب أن ترى رسالة في علامة التبويب Console في أسفل قسم Browser في CodeSandbox يقول "ضُغطت!"
. الضغط على المربع أكثر من مرة سيؤدي إلى طباعة "ضُغطت!"
مرة أخرى. تكرار console.log("ضُغطت!")
بنفس الرسالة لن ينشئ سطرًا جديدًا في الـ console. بدلاً من ذلك، سترى عدادًا متزايدًا بجانب أول طباعة "ضُغطت!"
لديك.
كخطوة تالية، تريد أن يتذكر مكون Square
أنه تم النقر عليه، وكتابة “X” فيه. “لتذكر” الأشياء، يستخدم المكونات الحالة (State).
تقدم React دالة خاصة تسمى useState
يمكنك استدعاؤها من مكونك لتمكنه “تذكر” الأشياء. دعنا نخزن القيمة الحالية لـ Square
في الحالة، ونغيرها عند النقر على Square
.
استيراد useState
في أعلى الملف. قم بإزالة خاصية value
من مكون Square
. بدلاً من ذلك، أضف سطرًا جديدًا في بداية Square
يستدعي useState
. اجعله يعيد متغير حالة يسمى value
:
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value
تحتفظ بالقيمة و setValue
هي الدالة التي تستخدم لتعديل القيمة. الـnull
المُمر إلى useState
يستخدم كالقيمة الأولية لمتغير الحالة هذا، لذا value
هنا يبدأ بالتساوي مع null
.
لأن مكون Square
لم يعد يقبل الخصائص (props) بعد الآن، ستقوم بإزالة خاصية value
من جميع المربعات التسعة التي أنشأها مكون 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("ضُغطت!");
بـ setValue('X');
. الآن يبدو مكون Square
الخاص بك على النحو التالي:
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
بمناداة هذه الدالة (Function) set
من معالج الحدث onClick
، أنت تخبر React بإعادة تقديم Square
كلما تم النقر على <button>
الخاص به. بعد التحديث، سيكون value
لـ Square
هو 'X'
، لذا سترى “X” على لوحة اللعب. انقر على أي مربع، وسيظهر “X”:
كل مربع له حالته الخاصة (state): الـ value
المخزنة في كل مربع مستقلة تمامًا عن الآخرين. عندما تستدعي دالة 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 DevTools)
أدوات مطور React تتيح لك التحقق من الخصائص والحالة لمكونات React الخاصة بك. يمكنك العثور على علامة تبويب أدوات مطوري React في أسفل قسم المتصفح في CodeSandbox:
لفحص مكون معين على الشاشة، استخدم الزر في الزاوية اليسرى العليا من أدوات مطوري React:
إكمال اللعبة
هنا، لديك كل الأساسيات لبناء لعبة التيك تاك تو. لإكمال اللعبة، تحتاج الآن إلى تبادل وضع “X” و “O” على اللوحة، وتحتاج إلى طريقة لتحديد الفائز.
رفع الحالة لأعلى (Lifting State Up)
حاليًا، كل مكون Square
يحتفظ بجزء من حالة اللعبة. للتحقق من وجود فائز في لعبة “تيك تاك تو”، سيحتاج Board
إلى معرفة حالة كل من مكونات Square
التسعة.
كيف ستحقق ذلك؟ في البداية، قد تخمن أن Board
يحتاج إلى “سؤال” كل Square
عن حالته. على الرغم من أن هذا النهج ممكن تقنيًا في React، إلا أننا ننصح بعدم استخدامه لأن الكود يصبح من الصعب فهمه، وعرضة للأخطاء (bugs)، وصعب التعديل. بدلاً من ذلك، أفضل نهج هو تخزين حالة اللعبة في مكون Board
الأصل بدلاً من كل Square
. يمكن لمكون Board
أن يخبر كل Square
ما يجب عليه عرضه عن طريق تمرير خاصية (prop)، مثلما فعلت عندما قمت بتمرير رقم إلى كل Square
.
لجمع بيانات من أطفال (children)، أو لجعل مكونين طفلين يتواصلان مع بعضهما البعض، قم بتعريف الحالة المشتركة في مكونهما الأصل بدلاً من ذلك. يمكن للمكون الأصلي أن يمرر هذه الحالة إلى الأطفال عن طريق الخصائص (props). هذا يحافظ على تزامن مكونات الأطفال مع بعضها البعض ومع مكونها الأصل.
رفع الحالة إلى مكون أصل (lifting state up) هو أمر شائع عند إعادة تنظيم مكونات React.
لنستغل هذه الفرصة لتجربتها. عدّل مكون Board
حتى يعلن عن متغير حالة يسمى squares
يبدأ بمصفوفة (array) من 9 قيم null
تتوافق مع 9 مربعات:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null)
تنشئ مصفوفة (array) من تسعة عناصر وتضبط كل منها على null
. استدعاء useState()
حولها يعلن عن متغير حالة squares
يتم تعيينه في البداية على هذه المصفوفة. كل إدخال في المصفوفة يتوافق مع قيمة مربع. عندما تملأ اللوحة لاحقًا، ستبدو المصفوفة 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
. هذا يتطلب إزالة تتبع مكون Square
لحالة value
وخاصية 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> </> ); }
كل مربع سيتلقى خاصية value
التي ستكون إما 'X'
أو 'O'
أو null
للمربعات الفارغة.
لاحقًا ستغير ما يحدث عند النقر على Square
. مكون Board
الآن يحتفظ بالمربعات المملوءة. ستحتاج إلى إيجاد طريقة لتحديث حالة Board
. لأن الحالة هي خاصة بالمكون الذي يحددها، لا يمكنك تحديث حالة Board
مباشرة من Square
.
بدلًا من ذلك، سنمرر دالة لأسفل من مكون Board
إلى مكون Square
، وسيقوم Square
باستدعاء تلك الدالة عند النقر على المربع. ستبدأ بالدالة التي سيقوم Square
بإستدعائها عند النقر عليه:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
الآن، ستضيف دالة onSquareClick
إلى خاصية Square
:
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
الآن ستوصل onSquareClick
إلى دالة في مكون Board
سنسميها handleClick
. لتوصل onSquareClick
إلى handleClick
سنمرر دالة إلى خاصية onSquareClick
لأول مكون Square
:
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()
في JavaScript. ثم، تقوم handleClick
بتحديث المصفوفة nextSquares
لإضافة X
إلى المربع الأول ([0]
).
مناداة دالة setSquares
تخبر React بأن حالة المكون قد تغيرت. هذا سيؤدي إلى إعادة رسم المكونات التي تستخدم حالة squares
(Board
) وكذلك مكوناتها الفرعية (مكونات Square
التي تشكل اللوحة).
الآن، يمكنك إضافة الـXـات إلى اللوحة… لكن فقط للمربع العلوي الأيسر. دالة handleClick
مبرمجة بشكل يدوي لتحديث المربع العلوي الأيسر (0
). دعنا نحدث handleClick
لتتمكن من تحديث أي مربع. أضف وسيطًا i
إلى دالة 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 (
// ...
)
}
الآن، ستحتاج إلى تمرير ذلك الـ i
إلى handleClick
. يمكنك أن تحاول تعيين خاصية onSquareClick
لمربع لتكون handleClick(0)
مباشرة في JSX مثل هذا، لكنه لن يعمل:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
هنا سبب عدم عمل ذلك. مناداة handleClick(0)
ستكون جزءًا من عملية إعادة رسم مكون اللوحة. لأن handleClick(0)
تقوم بتغيير حالة مكون اللوحة عن طريق استدعاء setSquares
، سيتم إعادة رسم مكون اللوحة مرة أخرى. لكن هذا سيقوم بتشغيل handleClick(0)
مرة أخرى، مما يؤدي إلى حدوث حلقة لا نهائية (infinite loop):
يمكنك إصلاح ذلك عن طريق إنشاء دالة مثل handleFirstSquareClick
التي تنادي handleClick(0)
، ودالة مثل handleSecondSquareClick
التي تنادي handleClick(0)
، وهكذا… ربما تمرر (بدلًا من مناداة) هذه الدوال كخصائص إلى المكونات الفرعية. مثل onSquareClick={handleFirstSquareClick}
. هذا قد يحل الدورة اللانهائية.
لماذا لم تحدث هذه المشكلة من قبل؟
عندما كنا نمرر onSquareClick={handleClick}
، كنا نمرر دالة handleClick
كخاصية. لم نكن نستدعيها! لكن الآن نحن نستدعي تلك الدالة على الفور —لاحظ الأقواس في handleClick(0)
— وهذا هو السبب في أنها تعمل مبكرًا جدًا. لا نريد أن نستدعي handleClick
حتى ينقر المستخدم!
يمكنك حلها عن طريق إنشاء دالة مثل handleFirstSquareClick
التي تستدعي handleClick(0)
، ودالة مثل handleSecondSquareClick
التي تستدعي handleClick(1)
، وهكذا. ستمرر (بدلاً من استدعاء) هذه الدوال كخصائص مثل onSquareClick={handleFirstSquareClick}
. هذا سيحل مشكلة الحلقة اللانهائية.
ومع ذلك، تعريف تسع دوال مختلفة وإعطاء كل منها اسمًا هو أمر طويل جدًا. بدلاً من ذلك، دعنا نفعل هذا:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
لاحظ الصيغة الجديدة () =>
. هنا، () => handleClick(0)
هي دالة سهمية (Arrow Function)، وهي طريقة أقصر لتعريف الدوال. عندما ينقر المربع، سيتم تشغيل الكود بعد السهم =>
، والذي سيستدعي handleClick(0)
.
الآن تحتاج إلى تعديل المربعات الثمانية الأخرى لاستدعاء handleClick
من الدوال السهمية التي تمررها. تأكد من أن الوسيط لكل استدعاء لـ handleClick
يتوافق مع فهرس المربع الصحيح:
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
!
هذا ما يجب أن يبدو عليه الكود الخاص بك:
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> </> ); }
الآن إدارة حالتك في مكون Board
، يمرر مكونك Board
الأصلي الخاصيات إلى مكونات Square
الفرعية كي يعرضها بشكل صحيح. عند النقر فوق Square
، يطلب مكون Square
الفرعي الآن من مكون Board
الأصلي تحديث حالة اللوحة. عندما تتغير حالة Board
، يتم إعادة تقديم كل من مكون Board
و Square
الفرعي تلقائيًا. إبقاء حالة جميع المربعات في مكون Board
سيسمح له بتحديد الفائز في المستقبل.
لنلخص ما يحدث عندما ينقر المستخدم على المربع الأيسر العلوي في اللوحة الخاصة بك لإضافة X
إليه:
- الضفط على المربع الأيسر العلوي يشغل الدالة التي تلقاها العنصر
<button>
كخاصيةonClick
من العنصر<Square>
، والذي تلقاها العنصر<Square>
كخاصيةonSquareClick
من العنصر<Board>
، والذي قام بتعريف تلك الدالة مباشرة في JSX. تقوم الدالة بإجراء استدعاء لـhandleClick
مع وسيطة0
. - تستخدم
handleClick
الوسيطة (0
) لتحديث العنصر الأول في مصفوفةsquares
منnull
إلىX
. - تم تحديث حالة
squares
في عنصر<Board>
، لذا يتم إعادة تقديم العنصر<Board>
وجميع عناصره الفرعية. وهذا يؤدي إلى تغيير خاصيةvalue
لعنصر<Square>
ذي الترتيب0
منnull
إلىX
.
في النهاية يرى المستخدم أن المربع الأيسر العلوي قد تغير من فارغ إلى X
بعد النقر عليه.
لماذا اللا تغييرية (Immutability) مهمة
تذكر كيف تقوم في handleClick
بالاتصال بـ .slice()
لإنشاء نسخة من مصفوفة squares
بدلاً من تعديل المصفوفة الحالية. لشرح السبب ، نحتاج إلى مناقشة اللا تغييرية (Immutability) ولماذا هي مهمة للتعلم.
عمومًا، هناك نهجان لتغيير البيانات. النهج الأول هو تغيير البيانات مباشرةً عن طريق تغيير قيم البيانات. النهج الثاني هو استبدال البيانات بنسخة جديدة تحتوي على التغييرات المطلوبة. هنا ما سيبدو عليه الأمر إذا قمت بتغيير مصفوفة squares
:
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// الآن `squares` هي ["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];
// الآن `squares` لم تتغير، لكن العنصر الأول في `nextSquares` هو 'X' بدلاً من `null`
النتيجة واحدة ولكن عن طريق عدم تغيير البيانات مباشرةً (تغيير البيانات الأساسية) ، تحصل على عدة فوائد.
عدم التغيير يجعل المميزات المعقدة أكثر سهولة في التنفيذ. لاحقًا في هذا الشرح التطبيقي، ستنفذ ميزة “السفر عبر الزمن” التي تتيح لك مراجعة تاريخ اللعبة و “الرجوع إلى الوراء” إلى الحركات السابقة. هذه الميزة ليست محدودة بالألعاب - القدرة على التراجع وإعادة الإجراءات ميزة شائعة للتطبيقات. عدم تغيير البيانات المباشر يتيح لك الاحتفاظ بالإصدارات السابقة من البيانات سليمة، وإعادة استخدامها لاحقًا.
هناك أيضًا فائدة أخرى لعدم التغيير. افترضيًا، كل العناصر الفرعية (الأبناء) تقوم بإعادة التصيير (re-render) تلقائيًا عندما يتغير حالة عنصر أب. هذا يشمل حتى العناصر الفرعية التي لم تتأثر بالتغيير. على الرغم من أن إعادة التصيير ليست ملحوظة للمستخدم (لا يجب عليك التحمس لمحاولة تجنبها!)، قد ترغب في تخطي إعادة تصيير جزء من الشجرة التي لم تتأثر به لأسباب أدائية (Performance). عدم التغيير يجعل من السهل جدًا على العناصر مقارنة ما إذا كانت بياناتها قد تغيرت أم لا. يمكنك معرفة المزيد حول كيفية اختيار 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
. إذا كان المربع ممتلئًا بالفعل، فستقوم بـ return
في دالة handleClick
مبكرًا - قبل محاولة تحديث حالة اللوحة.
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
يمكنك الآن إضافة X
أو O
إلى المربعات الفارغة فقط! هنا ما يجب أن يبدو عليه الكود الخاص بك في هذه المرحلة:
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> </> ); }
الإعلان عن الفائز
الآن بما أن اللاعبين يمكنهم التناوب، ستريد أن تظهر شيئا عندما يفوز لاعب وعند عدم وجود المزيد من المحاولات للعب. للقيام بذلك، ستضيف دالة مساعدة تسمى calculateWinner
تأخذ مصفوفة من 9 مربعات، وتتحقق من الفائز وتعيد 'X'
، 'O'
، أو null
حسب الحاجة. لا تقلق كثيرًا بشأن دالة calculateWinner
ليست شيئًا خاصًا بـ React. إنها مجرد JavaScript.
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(squares)
في دالة handleClick
الخاصة بمكون Board
للتحقق مما إذا كان اللاعب قد فاز. يمكنك تنفيذ هذا التحقق في نفس الوقت الذي تتحقق فيه مما إذا كان المستخدم قد نقر على مربع يحتوي بالفعل على X
أو O
. نود أن نوقف تنفيذ الدالة في كلا الحالتين:
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
لإعلام اللاعبين عندما تنتهي اللعبة، يمكنك عرض نص مثل “الفائز: X” أو “الفائز: O”. للقيام بذلك، ستضيف قسم status
إلى مكون Board
. سيعرض status
الفائز إذا انتهت اللعبة، وإذا كانت اللعبة لا تزال قائمة ستعرض أي لاعب عليه الدور في اللعب:
export default function Board() {
// ...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "الفائز هو: " + winner;
} else {
status = "اللاعب التالي: " + (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; } else { status = 'اللاعب التالي: ' + (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; }
إضافة السفر عبر الزمن
كتدريب أخير، دعنا نجعل من الممكن “العودة إلى الوراء” إلى الخطوات السابقة في اللعبة.
ترتيب تاريخ الانتقالات
إذا قمت بتغيير squares
array، فإن تنفيذ السفر عبر الزمن سيكون صعبًا جدًا.
ومع ذلك لقد استخدمنا slice()
لإنشاء نسخة جديدة من مصفوفة squares
array بعد كل خطوة، وعاملناها على أنها لا تتغير. هذا سيسمح لك بتخزين كل نسخة سابقة من مصفوفة squares
array، والتنقل بين اللعبات التي حدثت بالفعل.
ستخزن اللعبات السابقة لـ squares
في مصفوفة أخرى تسمى history
، والتي ستخزنها كمتغير حالة جديد. تمثل مصفوفة history
جميع حالات اللوحة، من الخطوة الأولى إلى الخطوة الأخيرة، ولها شكل مثل هذا:
[
// قبل الانتقال الأول
[null, null, null, null, null, null, null, null, null],
// بعد الانتقال الأول
[null, null, null, null, 'X', null, null, null, null],
// بعد الانتقال الثاني
[null, null, null, null, 'X', null, null, null, 'O'],
// ...
]
رفع الحالة لأعلى (Listing state up)، مرة أخرى
ستنشئ الآن مكونًا جديدًا على المستوى الأعلى يسمى Game
لعرض قائمة بالخطوات السابقة. هنا ستضع حالة history
التي تحتوي على تاريخ اللعبة بأكمله.
نقل history
إلى مكون Game
سيسمح لك بإزالة حالة squares
من مكون Board
الفرعي. تمامًا مثلما “رفعت الحالة لأعلى” من مكون Square
إلى مكون Board
، سترفعها الآن من Board
إلى مكون Game
على المستوى الأعلى. هذا يمنح مكون Game
السيطرة الكاملة على بيانات Board
ويتيح له Board
تقديم اللعبات السابقة من history
.
أولًا، أضف export default
إلى مكون Game
. ثم اجعله يقوم بتقديم مكون Board
وبعض الترميز (markup) الإضافي:
function Board() {
// ...
}
export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*مَهمَّة*/}</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)]
هي مصفوفة بها عنصر واحد، وهو بدوره مصفوفة من 9 من القيم null
.
[
[null, null, null, null, null, null, null, null, null]
]
لعرض مربعات الانتقالة الحالية، ستريد قراءة مصفوفة squares
الأخيرة من 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
كخصائص إلى مكون 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) {
// مَهمَّة
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
//...
)
}
لنجعل مكون Board
متحكمًّا به بالكامل من خلال الخصائص التي يستقبلها، عدّل مكون Board
ليأخذ ثلاث خصائص: xIsNext
و squares
ودالة onPlay
جديدة يمكن لـ Board
استدعاؤها مع مصفوفة المربعات المُحدَّثة عندما يلعب اللاعب. ثم، احذف السطرين الأولين من دالة Board
التي تستدعيان useState
:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
الآن استبدل استدعاءات setSquares
و setXIsNext
في handleClick
في مكون Board
باستدعاء واحد لدالتك الجديدة onPlay
حتى يتمكن مكون Game
من تحديث Board
عندما ينقر المستخدم على مربع:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
//...
}
مكون Board
مُتَحكَّمٌ به بالكامل من خلال الخصائص التي يتم تمريرها إليه من مكون Game
. ستحتاج إلى تنفيذ دالة handlePlay
في مكون Game
لجعل اللعبة تعمل مرة أخرى.
ماذا يجب أن تفعل handlePlay
عند استدعائها؟ تذكر أن مكون Board
كان يستدعي setSquares
مع مصفوفة مُحدَّثة؛ الآن يمرر مصفوفة squares
المُحدَّثة إلى onPlay
.
دالة handlePlay
تحتاج لتعديل حالة Game
لتشغيل إعادة التصيير (Re-rendering)، لكن ليس لديك دالة setSquares
يمكنك استدعاؤها بعد الآن - أنت تستخدم الآن متغير الحالة history
لتخزين هذه المعلومات. ستحتاج تحديث history
عن طريق إضافة مصفوفة squares
المُحدَّثة كإدخال جديد في history
. كما تحتاج تبديل xIsNext
، تمامًا كما كان يفعل Board
:
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
هنا، [...history, nextSquares]
تنشئ مصفوفة جديدة تحتوي على جميع العناصر في history
، تليها nextSquares
. (يمكنك قراءة ...history
Spread Operator كـ “جميع العناصر في history
”.)
علي سبيل المثال، إذا كانت history
هي [[null,null,null], ["X",null,null]]
وكان nextSquares
هو ["X",null,"O"]
، فإن المصفوفة الجديدة [...history, nextSquares]
ستكون [[null,null,null], ["X",null,null], ["X",null,"O"]]
.
في هذه النقطة، لقد نقلت الحالة لتكون في مكون 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; } else { status = 'اللاعب التالي: ' + (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>{/*مَهمَّة*/}</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 مثل <button>
هي كائنات JavaScript العادية؛ يمكنك تمريرها في تطبيقك. لتقديم عناصر متعددة في React، يمكنك استخدام مصفوفة من عناصر React.
لديك بالفعل مصفوفة من خطوات history
في الحالة، لذلك عليك الآن تحويلها إلى مصفوفة من عناصر React. في JavaScript، لتحويل مصفوفة واحدة إلى أخرى، يمكنك استخدام طريقة map
للمصفوفة:
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
ستستخدم map
لتحويل history
من الخطوات إلى عناصر React تمثل الأزرار على الشاشة، وعرض قائمة من الأزرار لـ “القفز” إلى الخطوات السابقة. دعنا نمرر map
على history
في مكون Game:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
function jumpTo(nextMove) {
// مَهمَّة
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'انتقل إلى الخطة #' + move;
} else {
description = 'انتقل إلى بداية اللعبة';
}
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>
);
}
يمكنك رؤية ما يجب أن يبدو عليه الكود أدناه. لاحظ أنه يجب أن ترى خطأ في أدوات المطورين يقول:
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Game`.
تحذير: يجب أن يكون لكل طفل في مصفوفة أو محدد خاصية "مفتاح key" فريدة. تحقق من طريقة التصيير لـ `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; } else { status = 'اللاعب التالي: ' + (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) { // مَهمَّة } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'انتقل إلى الخطوة #' + move; } else { description = 'انتقل إلى بداية اللعبة'; } 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; }
بمجرد أن تمر على مصفوفة history
بالدالة التي قمت بتمريرها إلى map
، فإن المعامل (argument) squares
يشير إلى كل عنصر من عناصر history
، والمعامل move
يشير إلى كل مسلسل في المصفوفة: 0
، 1
، 2
، …. (في معظم الحالات، ستحتاج إلى عناصر المصفوفة الفعلية، ولكن لتقديم قائمة من الخطوات، ستحتاج فقط إلى الفهارس).
لكل حركة في تاريخ لعبة “تيك تاك تو”، تنشئ عنصر قائمة <li>
يحتوي على زر <button>
. يحتوي الزر على حدث onClick
الذي يستدعي دالة تسمى jumpTo
(التي لم تقم بتنفيذها بعد).
إلى الآن، يجب أن ترى قائمة بالخطوات التي حدثت في اللعبة وخطأ في وحدة التحكم console. دعنا نناقش ما يعنيه خطأ “المفتاح”.
اختيار مفتاح
عندما تعرض قائمة، تخزن React بعض المعلومات عن كل عنصر في القائمة التي تم عرضها. عندما تقوم بتحديث قائمة، يحتاج React إلى تحديد ما تغيّر. ربما تكون أضفت، أو أزلت، أو رتبت أو عدلت عناصر القائمة.
تخيل الانتقال من
<li>عصام: 7 مهمات متبقية</li>
<li>هند: 5 مهمات متبقية</li>
إلى
<li>هند: 9 مهمات متبقية</li>
<li>سعيد: 8 مهمات متبقية</li>
<li>عصام: 5 مهمات متبقية</li>
بالإضافة إلى العدادات المحدثة، فإن الإنسان الذي يقرأ هذا سيقول على الأرجح أنك قمت بتبديل ترتيب عصام وهند، وأضفت سعيد بينهما، ومع ذلك React هو برنامج حاسوبي ولا يمكنه معرفة ما كنت تقصده، لذا تحتاج إلى تحديد خاصية مفتاحية key لكل عنصر في القائمة للتمييز بين كل عنصر في القائمة وإخوته. إذا كانت بياناتك من قاعدة بيانات، فيمكن استخدام معرفات قاعدة بيانات الفريدة (ID). أقصد المعرف الخاص بهند وعصام وسعيد.
عندما لا يكون لديك معرفات مستقرة لتصيير العناصر، ربما تستخدم المسلسل كمفتاح كحل أخير:
<li key={user.id}>
{user.name}: {user.taskCount} مهمات متبقية
</li>
عندما يتم إعادة تصيير القائمة، تأخد React كل مفتاح عنصر وتبحث في القائمة السابقة عن عنصر متطابق. إذا كانت القائمة الحالية تحتوي على مفتاح لم يكن موجودًا من قبل، فإن React ينشئ عنصرًا. إذا كانت القائمة الحالية تفتقد مفتاحًا كان موجودًا في القائمةالسابقة فإن React تزيل المكون السابق. إذا تطابق مفتاحان، فإن المكون المقابل يتم نقله.
تخبر المفاتيح React عن هوية كل مكون، مما يسمح لـ React الحفاظ على الحالة بين إعادة التصيير. إذا تغير مفتاح المكون، فإن المكون سيتم تدميره وإعادة إنشائه مع حالة جديدة.
key
هو خاصية محجوزة في React. عند إنشاء عنصر، تقوم React بإستخراج خاصية key
وتخزينها مباشرة على العنصر المعاد. على الرغم من أن key
قد يبدو كما لو أنه يمر كخاصية، إلا أن React تستخدم key
تلقائيًا لتحديد أي مكونات يجب تحديثها. لا يوجد طريقة للمكون لمعرفة ما هو key
الذي حدده المكون الأب.
نوصي بشدة تعيين مفاتيح مناسبة كلما قمت ببناء قوائم ديناميكية. إذا لم يكن لديك مفتاح مناسب، قد ترغب في إعادة هيكلة بياناتك ليكون لديك معرف فريد.
إذا لم يتم تحديد مفتاح، فإن React ستقوم بإرجاع خطأ واستخدام مؤشر المصفوفة كمفتاح افتراضي. استخدام مؤشر المصفوفة كمفتاح يسبب مشاكل عند محاولة إعادة ترتيب عناصر القائمة أو إضافة/إزالة عناصر للقائمة. تمرير key={i}
لا تظهر خطأً ولكن لها نفس المشاكل مثل مسلسلات المصفوفة ولا يوصى بها في معظم الحالات.
لا تحتاج المفاتيح إلى تكون فريدة مطلقًا على مستوى البرنامج (Global)، فهي تحتاج فقط إلى أن تكون فريدة بين المكونات وإخوتها.
تنفيذ السفر عبر الزمن
في تاريخ لعبة “تيك تاك تو” كل نقلة سابقة لها معرف فريد مرتبط بها: هو الرقم الترتيبي للنقلة. لن يتم إعادة ترتيب النقلات، أو حذفها، أو إدراجها في الوسط، لذلك فمن الآمن استخدام فهرس النقلة كمفتاح (index as a key).
في دالة Game
، يمكنك إضافة المفتاح كـ <li key={move}>
، وإذا قمت بإعادة تحميل اللعبة، فإن خطأ “key” في React يجب أن يختفي:
const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'الفائز هو: ' + winner; } else { status = 'اللاعب التالي: ' + (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) { // مَهمّةٌ! } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'انتقل إلى الحركة #' + move; } else { description = 'انتقل إلى بداية اللعبة'; } 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; }
قبل أن تنفذ 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
. ستعين أيضًا xIsNext
إلى true
إذا كان الرقم الذي تقوم بتغيير currentMove
إليه زوجيًا (يقبل القسمة على 2).
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
الآن، ستقوم بإجراء تغييرين على الدالة handlePlay
في المكوّن Game
التي تُستدعى عند النقر على مربّع.
- إذا “عدت إلى الوراء في الزمن” ثم قمت بإجراء حركة جديدة من هذه النقطة، فإنك تريد فقط الاحتفاظ بالتاريخ حتى هذه النقطة. بدلاً من إضافة
nextSquares
بعد جميع العناصر (...
spread syntax) فيhistory
، ستضيفها بعد جميع العناصر في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; } else { status = 'اللاعب التالي: ' + (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 = 'انتقل إلى خطوة #' + move; } else { description = 'انتقل إلى بداية اللعبة'; } 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; }
اللمسات النهائية
إذا نظرت عن قرب إلى الكود، فقد تلاحظ أن xIsNext === true
عندما يكون currentMove
زوجيًا و xIsNext === false
عندما يكون currentMove
فرديًا. بعبارة أخرى، إذا كنت تعرف قيمة currentMove
، فيمكنك دائمًا معرفة ما يجب أن يكون عليه xIsNext
.
ليس هناك سبب لتخزين كليهما في الحالة. في الواقع، حاول دائمًا تجنب تكرار الحالة. يقلل تبسيط ما تخزنه في الحالة من الأخطاء ويجعل من السهل فهم الكود الخاص بك. عدّل Game
بحيث لا يخزن 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
أو استدعاء setXIsNext
. الآن، لا يوجد مجال لتخلف xIsNext
عن currentMove
، حتى لو ارتكبت خطأ أثناء كتابة المكونات.
الاستنتاج
مبارك! لقد أنشأت لعبة “تيك تاك تو” تقوم بما يلي:
- تتيح لك لعب لعبة “تيك تاك تو”،
- تشير إلى أن لاعبًا فاز باللعبة،
- تخزن تاريخ اللعبة مع تقدم اللعبة،
- تسمح للاعبين بمراجعة تاريخ اللعبة ورؤية الإصدارات السابقة من لوحة اللعبة.
عمل رائع! نأمل أن لديك الآن فهمًا جيدًا لكيفية عمل 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; } else { status = 'اللاعب التالي: ' + (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 = 'انتقل إلى الخطوة #' + move; } else { description = 'انتقل إلى بداية اللعبة'; } 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، فإليك بعض الأفكار لتحسين لعبة “تيك تاك تو”، مرتبة حسب صعوبتها:
- في الخطوة الحالية فقط، اعرض “أنت في الخطوة #…” بدلاً من زر.
- أعد كتابة
Board
لاستخدام حلقتين (loop) لإنشاء المربعات بدلاً من كتابتها يدويًا. - أضف زر تبديل يتيح لك فرز الخطوات بترتيب تصاعدي أو تنازلي.
- عندما يفوز أحد، قم بتمييز المربعات الثلاثة التي تسببت في الفوز (وعندما لا يفوز أحد، عرض رسالة حول نتيجة اللعبة).
- عرض الموقع لكل خطوة في التنسيق (الصف، العمود) في قائمة تاريخ الخطوات.
طوال هذا الشرح التطبيقي لمست مفاهيم React بما في ذلك العناصر (elements)، والمكونات (components)، والخصائص (props)، والحالة (state). الآن بعد أن رأيت كيف تعمل هذه المفاهيم عند بناء لعبة، تفقد التفكير في React لمعرفة كيف تعمل نفس المفاهيم في React عند بناء واجهة مستخدم تطبيق.