الفرق بين المراجعتين لصفحة: «React/tutorial»
Kinan-mawed (نقاش | مساهمات) لا ملخص تعديل |
Kinan-mawed (نقاش | مساهمات) لا ملخص تعديل |
||
سطر 464: | سطر 464: | ||
مكوّنات الدوال في React هي طريقة أبسط لكتابة المكوّنات التي تحتوي فقط على تابع التصيير <code>render</code> بدون أن تمتلك حالتها الخاصّة. فبدلًا من تعريف صنف يمتد إلى الصنف <code>React.Component</code> نستطيع كتابة دالة تأخذ خاصيّات <code>props</code> وحقل إدخال وتُعيد ما ينبغي تصييره. من الأسهل كتابة مكوّنات الدوال بدلًا من الأصناف، ويُمكِن التعبير عن الكثير من المكوّنات بهذه الطريقة. | مكوّنات الدوال في React هي طريقة أبسط لكتابة المكوّنات التي تحتوي فقط على تابع التصيير <code>render</code> بدون أن تمتلك حالتها الخاصّة. فبدلًا من تعريف صنف يمتد إلى الصنف <code>React.Component</code> نستطيع كتابة دالة تأخذ خاصيّات <code>props</code> وحقل إدخال وتُعيد ما ينبغي تصييره. من الأسهل كتابة مكوّنات الدوال بدلًا من الأصناف، ويُمكِن التعبير عن الكثير من المكوّنات بهذه الطريقة. | ||
ضع هذه الدالة بدلًا من الصنف <code>Square</code>: | ضع هذه الدالة بدلًا من الصنف <code>Square</code>:<syntaxhighlight lang="javascript"> | ||
function Square(props) { | |||
return ( | |||
<button className="square" onClick={props.onClick}> | |||
{props.value} | |||
</button> | |||
); | |||
} | |||
</syntaxhighlight>غيّرنا <code>this.props</code> إلى <code>props</code> في المرّات التي ظهرت فيها. | |||
انظر إلى كامل الشيفرة عند هذه النقطة. | |||
ملاحظة: عندما عدّلنا المكوّن Square ليصبح مكوّن دالة، فقد غيّرنا أيضًا <code>onClick={() => this.props.onClick()}</code> إلى الشكل المختصر <code>onClick={props.onClick}</code> (لاحظ عدم وجود الأقواس على الجانبين). في حال الصنف استخدمنا الدوال السهميّة للوصول إلى قيمة <code>this</code> الصحيحة، ولكن في مكوّنات الدوال لا حاجة للقلق حول <code>this</code>. | |||
=== أخذ الأدوار === | |||
نحتاج الآن لإصلاح عيب واضح في لعبة إكس-أو لدينا، فلا يُمكِن وضع الإشارة <code>O</code> على لوحة اللعبة. | |||
سنُعيِّن أول خطوة لتكون <code>X</code> افتراضيًّا. نستطيع تعيين هذه القيمة الافتراضيّة عن طريق تعديل الحالة المبدئيّة في الدالة البانية للمكوّن <code>Board</code>:<syntaxhighlight lang="javascript"> | |||
class Board extends React.Component { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
squares: Array(9).fill(null), | |||
xIsNext: true, | |||
}; | |||
} | |||
</syntaxhighlight>في كل مرة يتحرّك بها اللاعب ستنقلب قيمة المتغيّر <code>xIsNext</code> (متغير منطقي) لتحديد أي لاعب سيلعب الخطوة التالية وستُحفَظ حالة اللعبة. سنُحدِّث الدالة <code>handleClick</code> للمكوّن <code>Board</code> لتقلب قيمة <code>xIsNext</code>:<syntaxhighlight lang="javascript"> | |||
handleClick(i) { | |||
const squares = this.state.squares.slice(); | |||
squares[i] = this.state.xIsNext ? 'X' : 'O'; | |||
this.setState({ | |||
squares: squares, | |||
xIsNext: !this.state.xIsNext, | |||
}); | |||
} | |||
</syntaxhighlight>بها التغيير تستطيع <code>X</code> و <code>O</code> أخذ الأدوار. فلنُغيِّر أيضًا نص الحالة في التابع <code>render</code> للمكوّن <code>Board</code> بحيث يعرض من هو اللاعب الذي سيلعب الدور التالي:<syntaxhighlight lang="javascript"> | |||
render() { | |||
const status = 'اللاعب التالي: ' + (this.state.xIsNext ? 'X' : 'O'); | |||
return ( | |||
// لم تتغير بقية الشيفرة | |||
</syntaxhighlight>بعد تطبيق هذه التغييرات يجب أن تملك مكوّن <code>Board</code> مماثل لما يلي:<syntaxhighlight lang="javascript"> | |||
class Board extends React.Component { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
squares: Array(9).fill(null), | |||
xIsNext: true, | |||
}; | |||
} | |||
handleClick(i) { | |||
const squares = this.state.squares.slice(); | |||
squares[i] = this.state.xIsNext ? 'X' : 'O'; | |||
this.setState({ | |||
squares: squares, | |||
xIsNext: !this.state.xIsNext, | |||
}); | |||
} | |||
renderSquare(i) { | |||
return ( | |||
<Square | |||
value={this.state.squares[i]} | |||
onClick={() => this.handleClick(i)} | |||
/> | |||
); | |||
} | |||
render() { | |||
const status = 'اللاعب التالي: ' + (this.state.xIsNext ? 'X' : 'O'); | |||
return ( | |||
<div> | |||
<div className="status">{status}</div> | |||
<div className="board-row"> | |||
{this.renderSquare(0)} | |||
{this.renderSquare(1)} | |||
{this.renderSquare(2)} | |||
</div> | |||
<div className="board-row"> | |||
{this.renderSquare(3)} | |||
{this.renderSquare(4)} | |||
{this.renderSquare(5)} | |||
</div> | |||
<div className="board-row"> | |||
{this.renderSquare(6)} | |||
{this.renderSquare(7)} | |||
{this.renderSquare(8)} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} | |||
</syntaxhighlight>انظر إلى كامل الشيفرة عند هذه النقطة. | |||
=== التصريح عن الفائز === | |||
الآن بعد أن عرضنا من هو اللاعب الذي سيلعب الدور التالي، فيجب علينا أن نعرض عبارة عندما يفوز اللاعب باللعبة ولا تتبقى أيّة أدوار للعبها. نستطيع تحديد الفائز عن طريق إضافة هذه الدالة المساعدة إلى نهاية الملف:<syntaxhighlight lang="javascript"> | |||
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; | |||
} | |||
</syntaxhighlight>سنستدعي التابع <code>calculateWinner(squares)</code> في تابع التصيير <code>render</code> للمكوّن <code>Board</code> للتحقّق من فوز اللاعب. إن فاز اللاعب فنستطيع عرض نص مثل "الفائز: X" أو "الفائز: O". سنضع هذه الشيفرة بدلًا من تصريح الحالة الموجود في التابع <code>render</code> للمكوّن <code>Board</code>:<syntaxhighlight lang="javascript"> | |||
render() { | |||
const winner = calculateWinner(this.state.squares); | |||
let status; | |||
if (winner) { | |||
status = 'الفائز: ' + winner; | |||
} else { | |||
status = 'اللاعب التالي: ' + (this.state.xIsNext ? 'X' : 'O'); | |||
} | |||
return ( | |||
// لم تتغير بقية الشيفرة | |||
</syntaxhighlight>نستطيع الآن تغيير الدالة <code>handleClick</code> للمكوّن <code>Board</code> لتُعيد قيمتها باكرًا عن طريق تجاهل النقرة إن فاز أحد باللعبة أو إن كان المربّع يحتوي على قيمة مسبقًا:<syntaxhighlight lang="javascript"> | |||
handleClick(i) { | |||
const squares = this.state.squares.slice(); | |||
if (calculateWinner(squares) || squares[i]) { | |||
return; | |||
} | |||
squares[i] = this.state.xIsNext ? 'X' : 'O'; | |||
this.setState({ | |||
squares: squares, | |||
xIsNext: !this.state.xIsNext, | |||
}); | |||
} | |||
</syntaxhighlight>انظر إلى كامل الشيفرة عند هذه النقطة. | |||
تهانينا! تمتلك الآن لعبة إكس-أو تعمل بشكل جيّد. وقد تعلّمت أساسيّات React أيضًا. لذا قد تكون أنت الرابح الحقيقي هنا. | |||
== إضافة السفر عبر الزمن == | |||
كتمرين أخير فلنجعل من الممكن الرجوع إلى الخلف بالوقت إلى التحركات السابقة في اللعبة. | |||
=== تخزين تاريخ التحركات === | |||
إن عدّلنا المصفوفة <code>squares</code> سيكون تنفيذ السفر عبر الزمن أمرًا صعبًا. | |||
ولكننا استخدمنا التابع <code>slice()</code> لإنشاء نسخة من المصفوفة <code>squares</code> بعد كل تحرّك، والتعامل معها كمصفوفة غير قابلة للتعديل. يسمح لك ذلك بتخزين كل إصدار قديم من هذه المصفوفة، والتنقل بين الأدوار التي حدثت سابقًا. | |||
سنُخزِّن مصفوفات <code>squares</code> السابقة ضمن مصفوفة أخرى تُدعى <code>history</code> والتي تُمثِّل حالات لوحة اللعبة من أو إلى آخر تحرّك، ويكون شكلها كما يلي:<syntaxhighlight lang="javascript"> | |||
history = [ | |||
// قبل التحرك الأول | |||
{ | |||
squares: [ | |||
null, null, null, | |||
null, null, null, | |||
null, null, null, | |||
] | |||
}, | |||
// بعد التحرك الأول | |||
{ | |||
squares: [ | |||
null, null, null, | |||
null, 'X', null, | |||
null, null, null, | |||
] | |||
}, | |||
// بعد التحرك الثاني | |||
{ | |||
squares: [ | |||
null, null, null, | |||
null, 'X', null, | |||
null, null, 'O', | |||
] | |||
}, | |||
// ... | |||
] | |||
</syntaxhighlight>نحتاج الآن إلى أن نقرّر أي مكوّن ينبغي أن يمتلك الحالة <code>history</code>. | |||
=== رفع الحالة مرّة أخرى === | |||
نريد من المكوّن الموجود في أعلى مستوى من اللعبة أن يعرض قائمة بالتحركات السابقة. سيحتاج إلى الوصول إلى الحالة <code>history</code> لفعل ذلك، لذا سنضعها في المكوّن ذو المستوى الأعلى في اللعبة واسمه <code>Game</code>. | |||
يُتيح لنا وضع الحالة <code>history</code> في المكوّن <code>Game</code> أن نزيل الحالة <code>squares</code> من المكوّن الابن له وهو <code>Board</code>. كما رفعنا الحالة من المكوّن <code>Square</code> إلى <code>Board</code>، سنرفعها الآن من <code>Board</code> إلى المكوّن ذو المستوى الأعلى في اللعبة <code>Game</code>. يُعطي هذا المكوّن <code>Game</code> التحكّم الكامل ببيانات المكوّن <code>Board</code>، ويسمح له بأن يأمر المكوّن <code>Board</code> بتصيير الأدوار السابقة من <code>history</code>. | |||
سنُعِد في البداية الحالة المبدئيّة للمكوّن <code>Game</code> ضمن الدالة البانية له:<syntaxhighlight lang="javascript"> | |||
class Game extends React.Component { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
history: [{ | |||
squares: Array(9).fill(null), | |||
}], | |||
xIsNext: true, | |||
}; | |||
} | |||
render() { | |||
return ( | |||
<div className="game"> | |||
<div className="game-board"> | |||
<Board /> | |||
</div> | |||
<div className="game-info"> | |||
<div>{/* status */}</div> | |||
<ol>{/* TODO */}</ol> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} | |||
</syntaxhighlight> |
مراجعة 14:40، 15 سبتمبر 2018
لا يفترض هذا الدليل أي معرفة مسبقة بمكتبة React.
قبل أن نبدأ بالدليل التطبيقي
سنبني لعبة صغيرة خلال هذا الدليل التطبيقي. ربّما قد ترغب بتخطي هذا الدليل لأنّك لا تريد بناء الألعاب، ولكن أعطيها فرصة. إنّ التقنيات التي ستتعلمها في هذا الدليل أساسيّة لبناء أي تطبيق React، وسيعطيك إتقانها فهمًا أعمق لمكتبة React.
فائدة: هذا الدليل مُصمَّم للأشخاص الذين يُفضّلون التعلّم بالممارسة. إن كنت تُفضّل تعلّم المفاهيم من البداية فارجع إلى توثيق React من البداية خطوة بخطوة. قد تجد هذا الدليل متكاملًا مع توثيق React.
هذا الدليل مقسوم إلى عدّة أقسام:
- يُعطيك قسم الإعداد من أجل الدليل نقطة بداية لمتابعة الدليل.
- يُعلّمك قسم لمحة عامّة أساسيات React: المكوّنات، والخاصيّات، والحالة.
- يُعلِّمك قسم إكمال اللعبة أشيع التقنيات في تطوير React.
- يُعطيك قسم إضافة السفر عبر الزمن نظرة أعمق إلى نقاط القوة الفريدة لمكتبة React.
لا يجب عليك إكمال جميع الأقسام دفعة واحدة للحصول على الفائدة المرجوة من هذا الدليل. حاول الذهاب أبعد ما يمكن حتى ولو كان قسمًا أو قسمين.
لا بأس من نسخ ولصق الشيفرة عند متابعتك مع هذا الدليل، ولكن نوصي أن تكتبها بيدك. سيُساعدك ذلك بتطوير ذاكرتك وبإعطائك فهمًا أعمق لمكتبة React.
ماذا سنبني؟
سنرى في هذا الدليل كيفيّة بناء لعبة إكس-أو (اسمها بالإنجليزيّة tic-tac-toe) باستخدام React.
بإمكانك أن ترى النتيجة النهائيّة لما سنبنيه من هنا. إن كانت الشيفرة غير مفهومة بالنسبة لك أو كنتَ غير متآلف مع صياغة الشيفرة، فلا تقلق! فالهدف من هذا الدليل هو مساعدتك على فهم React وصياغتها.
نوصي بأن تلقي نظرة على لعبة إكس-أو قبل المتابعة. إحدى الميزات التي ستلاحظها وجود قائمة مُرقّمة على يمين لوحة اللعبة. تُعطيك هذه القائمة سجلًّا عن كل التحركات التي حصلت في اللعبة، وتُحدَّث بينما تستمر اللعبة.
تستطيع إغلاق لعبة إكس-أو بعدما تتآلف معها. سننطلق من قالب بسيط في هذا الدليل. خطوتنا التالية هي إتمام الإعداد لكي تستطيع البدء ببناء اللعبة.
المتطلّبات الأساسيّة
سنفترض أنّك متآلف مع HTML و JavaScript، ولكن يجب أن تكون قادرًا على المتابعة حتى ولو كنت قادمًا من لغة برمجة أخرى. سنفترض أنّك متآلف مع المفاهيم البرمجيّة مثل الدوال، والكائنات، والمصفوفات، وبدرجة أقل الأصناف.
إن احتجت لمراجعة JavaScript نوصيك بالرجوع إلى توثيق JavaScript في موسوعة حسوب. لاحظ أنّنا نستخدم بعض الميزات من ES6، وهي إصدار جديد من JavaScript. سنستخدم في هذا الدليل الدوال السهمية، الأصناف، والتصريحين let
و const
. بإمكانك استخدام Babel REPL لتتحقّق إلى ماذا تُصرَّف شيفرة ES6.
الإعداد من أجل الدليل
هناك طريقتان لإكمال هذا الدليل، بإمكانك إمّا كتابة الشيفرة في متصفحك، أو إعداد بيئة تطوير محلية على حاسوبك.
الخيار الأول للإعداد: كتابة الشيفرة في المتصفح
هذه أسرع طريقة للبدء.
في البداية افتح هذه الشيفرة المبدئيّة في نافذة جديدة. يجب أن تعرض النافذة الجديدة لوحة لعبة إكس-أو وشيفرة React. سنُعدِّل شيفرة React في هذا الدليل.
بإمكانك الآن تجاوز الخيار الثاني للإعداد والذهاب إلى قسم لمحة عامّة للحصول على لمحة عامّة عن React.
الخيار الثاني للإعداد: بيئة التطوير المحليّة
هذا الخيار اختياري وغير مطلوب من أجل هذا الدليل.
اختياري: تعليمات للمتابعة بشكل محلي باستخدام مُحرِّر النصوص المفضّل لديك
يتطلّب هذا الإعداد المزيد من العمل ولكنّه يسمح لك بمتابعة هذا الدليل باستخدام مُحرِّر نصوص من اختيارك. هذه هي الخطوات التي يجب عليك اتباعها:
- تأكّد من امتلاكك لأحدث إصدار من Node.js.
- اتبع تعليمات التثبيت لإنشاء تطبيق React لصنع مشروع جديد
npm install -g create-react-app
create-react-app my-app
- احذف كافة الملفات الموجودة في المجلّد
src/
للمشروع الجديد (لا تحذف هذا المجلّد، فقط محتوياته).
cd my-app
rm -f src/*
- أضف ملفًّا يُدعى
index.css
في المجلّدsrc/
مع وضع شيفرة CSS هذه ضمنه. - أضف ملفًّا يُدعى
index.js
في المجلّدsrc/
مع وضع شيفرة JavaScript هذه ضمنه. - أضف هذه الأسطر الثلاثة إلى بداية الملف
index.js
في المجلّدsrc/
:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
إن نفّذت الآن الأمر npm start
في مجلّد المشروع وفتحت الرابط http://localhost:3000
في المتصفّح، فبإمكانك أن ترى حقل فارغ للعبة إكس-أو.
نوصي بمتابعة هذه التعليمات لإعداد ميّزة تعليم الصياغة (syntax highlighting) في مُحرِّر النصوص لديك.
ساعدني أنا عالق!
إن وجدت أيّة صعوبات، تحقّق من مصادر مجتمع React، بالأخص دردشة Reactiflux هي طريقة رائعة للحصول على المساعدة بسرعة. إن لم تتلقى أي إجابة أو بقيت عالقًا عند مشكلة ما، يُرجى تقديم المشكلة وسنساعدك في حلّها.
لمحة عامّة
ما هي React؟
React هي مكتبة JavaScript مرنة، وفعّالة، وتصريحيّة لبناء واجهات المستخدم. تُتيح لك تركيب واجهات مستخدم مُعقّدة من قطع معزولة وصغيرة من الشيفرة تُدعى المكوّنات (components).
تمتلك React أنواع مختلفة من المكوّنات، ولكن سنبدأ بالمكوّنات التي هي أصناف فرعية من الصنف React.Component
:
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>قائمة تسوق لأجل {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}
// مثال عن استخدام المكون <ShoppingList name="Mark" />
سنتحدّث قريبًا عن هذه العناصر التي تشبه عناصر XML. نستخدم المكوّنات لنخبر React ما الذي نرغب برؤيته على الشاشة. عندما تتغيّر بياناتنا ستُحدِّث React بكفاءة وتُعيد تصيير مكوّناتنا.
في المثال السابق ShoppingList
هي مكوّن React على شكل صنف، وهي من نوع المكوّنات في React. يأخذ المكوّن مُعامِلات تُدعى الخاصيّات props
(اختصارًا للكلمة properties)، وتُعيد تسلسل هيكلي من المشاهد التي يجب عرضها عبر التابع render
.
يُعيد تابع التصيير render
وصفًا لما ترغب برؤيته على الشاشة. تأخذ React الوصف وتعرض النتيجة. يُعيد التابع render
بشكلٍ خاص عنصر React، والذي هو وصف بسيط لما ترغب بتصييره. يستخدم معظم مطوري React صياغة خاصّة تُدعى JSX والتي تُسهِّل عمليّة كتابة مثل هذه البُنى. فمثلًا تُحوَّل الصياغة <div />
في زمن البناء إلى React.createElement('div')
. يُكافِئ المثال السابق ما يلي:
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 children ... */),
React.createElement('ul', /* ... ul children ... */)
);
انظر إلى الإصدار الكامل الموسّع من هذا المثال.
إن كنت فضوليًّا فالتابع createElement()
مشروح بالتفصيل في مرجع واجهة برمجة التطبيق في React، ولكنّنا لن نستخدمه في هذا الدليل، بل سنستمر في استخدام JSX بدلًا من ذلك.
تأتي JSX مع القوة الكاملة للغة JavaScript. بإمكانك وضع أي تعابير JavaScript ضمن الأقواس بداخل JSX. كل عنصر React هو كائن JavaScript تستطيع تخزينه في متغيّر أو تمريره ضمن برنامجك.
يُصيِّر المكوّن ShoppingList
المذكور في الأعلى مكوّنات مُضمَّنة في DOM مثل <div />
و <li />
، ولكن تستطيع تركيب وتصيير مكوّنات React مُخصَّصة أيضًا. على سبيل المثال تستطيع الآن الإشارة إلى كامل قائمة التسوّق عن طريقة كتابة <ShoppingList />
. يكون كل مكوّن React مُغلّفًا وبإمكانه العمل بشكل مستقل. يسمح لك ذلك ببناء واجهات مستخدم مُعقّدة من مكوّنات بسيطة.
تفحّص شيفرة البدء
إن كنت ستجرّب شيفرة الدليل التطبيقي على متصفحك، افتح شيفرة البدء في نافذة جديدة. أمّا إن كنت ستجرّبها بشكل محلّي فافتح الملف src/index.js
الموجود في مجلّد مشروعك (لقد تعرّفت مسبقًا على هذا الملف خلال خطوة الإعداد).
شيفرة البدء هذه هي الأساس لما نبنيه. زوّدناك بتنسيق CSS لكي تحتاج للتركيز فقط على تعلّم React وبرمجة لعبة إكس-أو.
ستلاحظ بتفحّص الشيفرة أنّنا نمتلك ثلاثة مكوّنات:
- مكوّن المربّع
Square
. - مكوّن لوحة اللعبة
Board
. - مكوّن اللعبة
Game
.
يُصيِّر المكوّن Square
عنصر زر <button>
واحد، ويُصيِّر المكوّن Board
تسعة مربّعات. يُصيِّر المكوّن Game
لوحة مع وجود قيم للتلميح والتي سنُعدّلها لاحقًا. لا يوجد حتى الآن أي مكوّن تفاعلي.
تمرير البيانات عبر الخاصيّات
لتدريب أنفسنا فلنحاول تمرير بعض البيانات من المكوّن Board
إلى المكوّن Square
.
في التابع renderSquare
الموجود في المكوّن Board
، غيّر الشيفرة لتمرير خاصيّة تُدعى value
إلى المكوّن Square
:
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
غيّر التابع render
في المكوّن Square
لإظهار القيم عن طريق وضع {this.props.value}
بدلًا من {/* TODO */}
:
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value}
</button>
);
}
}
قبل التغييرات:

بعد التغييرات: يجب أن ترى عددًا في كل مربّع من الناتج المُصيَّر:

انظر إلى كامل الشيفرة عند هذه النقطة.
تهانينا! لقد مرّرت الآن خاصيّة prop
من المكوّن Board
الأب إلى المكوّن Square
الابن. تمرير الخاصيّات هو طريقة عبور المعلومات في تطبيقات React، من المكوّنات الآباء إلى المكوّنات الأبناء.
صنع مكوّن تفاعلي
فلنملأ المكوّن Square
بإشارة X
عند النقر عليه. فلنغيّر أولًا العنصر button
الذي يُعاد من تابع التصيير الخاص بالمكوّن Square
إلى ما يلي:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={function() { alert('نقرة'); }}>
{this.props.value}
</button>
);
}
}
إن نقرنا الآن على المربّع فيجب أن نحصل على تحذير في متصفحنا. ملاحظة: لتوفير الكتابة وتجنّب السلوك المُربِك لهذا، سنستخدم صياغة الدوال السهمية من أجل مُعالِجات الأحداث هنا وحتى في باقي أجزاء الشيفرة:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={() => alert('نقرة')}>
{this.props.value}
</button>
);
}
}
لاحظ كيف أنّنا في الشيفرة onClick={() => alert('click')}
نُمرِّر دالة إلى الخاصيّة onClick
. وهي تُطلَق فقط عند النقر. من الشائع نسيان () =>
وكتابة onClick={alert('click')}
، والذي يُؤدّي إلى إطلاق التحذير في كل مرّة يُعاد فيها تصيير المكوّن.
في الخطوة التالية سنريد من المكوّن Square
أن يتذكر أنّه نُقِر عليه وأن يملأ نفسه بالعلامة X
. لتذكر الأشياء تستخدم المكوّنات الحالة state
.
تستطيع مكوّنات React أن تمتلك حالة عن طريق تعيين this.state
في الدالة البانية لها. يجب اعتبار this.state
خاصّة (private) بالنسبة لمكوّن React المُعرَّفة ضمنه. فلنُخزِّن القيمة الحاليّة للمربّع في this.state
ونُغيّرها عند النقر عليه.
سنضيف في البداية دالة بانية إلى الصنف لتهيئة الحالة:
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button className="square" onClick={() => alert('نقرة')}>
{this.props.value}
</button>
);
}
}
ملاحظة: يجب عليك دومًا في أصناف JavaScript أن تستدعي الكلمة super
عند تعريف الدالة البانية للصنف الفرعي. يجب أن تبدأ جميع مكوّنات الأصناف في React التي تمتلك دالة بانية constructor
باستدعاء super(props)
.
سنُغيّر الآن تابع التصيير render
للمكوّن Square
ليعرض قيمة الحالة الحاليّة عند النقر عليه:
- ضع
this.state.value
بدلًا منthis.props.value
بداخل العنصر <button>
. - ضع
() => this.setState({value: 'X'})
بدلًا من() => alert()
. - ضع الخاصيّتين
className
وonClick
في أسطر منفصلة لسهولة القراءة.
بعد هذه التغييرات سيبدو العنصر <button>
المُعاد من تابع التصيير للمكوّن Square
كما يلي:
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button
className="square"
onClick={() => this.setState({value: 'X'})}
>
{this.state.value}
</button>
);
}
}
عن طريق استدعاء this.setState
من مُعالِج الأحداث onClick
في تابع التصيير للمكوّن Square
، نُخبِر React بأن تُعيد تصيير المكوّن Square
عند النقر على الزر <button>
الخاص به. بعد التحديث ستُصبِح قيمة this.state.value
هي 'X'
، لذا سنرى 'X'
في لوحة اللعبة. إن نقرت على أي مربّع يجب أن تظهر الإشارة X
.
عندما تستدعي التابع setState
في المكوّن، تُحدِّث React بشكل تلقائي المكوّنات الأبناء بداخله أيضًا.
انظر إلى كامل الشيفرة عند هذه النقطة.
أدوات المطوّر
تُتيح لك إضافة أدوات تطوير React من أجل متصفّح Chrome و Firefox أن تتفحّص شجرة مكوّنات React باستخدام أدوات تطوير المتصفّح:
صورة
تُتيح لك أدوات تطوير React التحقّق من خاصيّات وحالة المكوّنات.
تستطيع بعد تثبيت أدوات تطوير React أن تنقر بالزر الأيمن على أي عنصر في الصفحة، ثمّ تضغط على كلمة "Inspect" لفتح أدوات المطوّر، وستظهر نافذة React كآخر نافذة إلى اليمين.
لاحظ وجود بعض الخطوات الإضافيّة لكي تعمل الأدوات مع CodePen:
- سجّل الدخول إلى الموقع أو سجّل في الموقع مع تأكيد بريدك الإلكتروني (مطلوب لتجنّب البريد المزعج spam).
- اضغط على الزر "Fork".
- اضغط على "Change View" واختر بعدها "Debug mode".
- ستملك الآن أدوات التطوير نافذة React ضمن النافذة الجديدة التي ستفتح.
إكمال اللعبة
لدينا الآن أساسات البناء للعبة إكس-أو. للحصول على لعبة كاملة سنحتاج الآن إلى وضع إشارات X
و O
على لوحة اللعبة، وإلى إيجاد طريقة لتحديد الفائز.
رفع الحالة للمستوى الأعلى
يحتفظ حاليًّا كل مكوّن مربّع Square
بحالة اللعبة. لتحديد الفائز يجب علينا إبقاء قيمة كل مربّع من المربّعات التسعة في مكان واحد.
قد تعتقد أنّه من الأفضل أن يسأل مكوّن لوحة اللعبة Board
كل مكوّن مربّع Square
عن حالته. على الرغم من أنّ هذه الطريقة ممكنة في React ولكنّنا لا نفضّلها لأنّ الشيفرة ستصبح صعبة الفهم، وقابلة لوجود أخطاء فيها، ومن الصعب إعادة استخدامها. أفضل طريقة هي تخزين حالة اللعبة في مكوّن لوحة اللعبة Board
واعتباره مكوّن أب بدلًا من تخزينها في كل مربّع. يتمكّن المكوّن Board
من إخبار كل مربّع بما يجب عرضه عن طريق تمرير خاصيّة prop
، كما فعلنا عندما مرّرنا عددًا لكل مربّع.
لتجميع البيانات من المكوّنات الأبناء المتعددة، ولامتلاك مكوّنين أبناء يتواصلان مع بعضهما البعض، يجب التصريح عن الحالة المشتركة في المكوّن الأب لها. يستطيع المكوّن الأب تمرير الحالة إلى المكوّنات الأبناء له عن طريق الخاصيّات. يُبقي هذا المكوّنات الأبناء بحالة تزامن مع بعضها ومع المكوّن الأب.
رفع الحالة إلى المكوّن الأب هو أسلوب شائع عند إعادة تصنيع المكوّنات (refactoring). سنضيف دالة بانية إلى لوحة اللعبة وسنُعيّن الحالة المبدئيّة لمكوّن اللوحة كي تحتوي على مصفوفة تتضمّن 9 قيم null
. تتوافق هذه القيم التسعة مع المربعات التسعة الموجودة في اللعبة:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
renderSquare(i) {
return <Square value={i} />;
}
render() {
const status = 'اللاعب التالي: X';
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
عندما نملأ لوحة اللعبة لاحقًا فستبدو كما يلي:
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]
يبدو حاليًّا التابع renderSquare
الموجود في المكوّن Board
كما يلي:
renderSquare(i) {
return <Square value={i} />;
}
في البداية مرّرنا الخاصيّة value
من المكوّن Board
لإظهار الأعداد من 0 إلى 8 في كل مربّع، وفي خطوة سابقة وضعنا إشارات X
بدلًا من الأعداد، حيث يُحدِّد هذه الإشارة حالة المكوّن Sqaure
. ولهذا يتجاهل حاليًّا الخاصيّة value
المُمرَّرة إليه عن طريق المكوّن Board
.
سنستخدم الآن آليّة تمرير الخاصيّة مرّة أخرى. سنُعدِّل المكوّن Board
ليستعلم من كل مربّع عن قيمته الحاليّة (X
، أو O
، أو null
). لقد عرّفنا مسبقًا المصفوفة squares
في الدالة البانية للمكوّن Board
، وسنُعدِّل التابع renderSquare
الموجود ضمنه ليقرأ من تلك المصفوفة:
renderSquare(i) {
return <Square value={this.state.squares[i]} />;
}
انظر إلى كامل الشيفرة عند هذه النقطة.
سيستقبل كل مربّع الآن الخاصيّة value
والتي ستكون إمّا X
، أو O
، أو null
بالنسبة للمربّعات الفارغة.
سنحتاج الآن إلى تغيير ما يحدث عند النقر على المربّع. يعرف الآن المكوّن Board
ما هي المربّعات الممتلئة. يجب علينا إيجاد طريقة للمكوّن Square
لكي يُحدِّث حالة المكوّن Board
. بما أنّ الحالة تُعتبر خاصّة (private) للمكوّن الذي يُعرفها، فلن نستطيع تحديث حالة المكوّن Board
مباشرةً من المكوّن Square
.
للحفاظ على خصوصيّة حالة المكوّن Board
سنُمرِّر دالة من المكوّن Board
إلى Square
. تُستدعى هذه الدالة عند النقر على المربّع. سنغيّر التابع renderSquare
في المكوّن Board
إلى:
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
ملاحظة: نُقسِّم العنصر المُعاد إلى عدّة أسطر لسهولة القراءة، وأضفنا أقواس لكي لا تُدخِل JavaScript فاصلة منقوطة بعد الكلمة return
وتؤدي إلى حدوث خطأ في شيفرتنا.
نُمرِّر الآن خاصيتين من المكوّن Board
إلى Square
وهما value
و onClick
. الخاصيّة onClick
هي عبارة عن دالة يستطيع المكوّن Square
استدعاءها عند النقر عليه. سنجري التغييرات التالية بالمكوّن Square
:
- نضع
this.props.value
بدلًا منthis.state.value
في تابع التصييرrender
له. - نضع
this.props.onClick()
بدلًا منthis.setState()
في تابع التصييرrender
له. - نحذف الدالة البانية
constructor
منه لأنّ المربّع لم يعد يحتاج لتتبع حالة اللعبة.
سيبدو المكوّن Square
بعد التغييرات كما يلي:
class Square extends React.Component {
render() {
return (
<button
className="square"
onClick={() => this.props.onClick()}
>
{this.props.value}
</button>
);
}
}
عند النقر على المربّع تُستدعى الدالة onClick
المُزوَّدة من قبل المكوّن Board
. وهذا ملخّص لكيفية تحقيق ذلك:
- تُخبِر الخاصيّة
onClick
الموجودة في المكوّن<button>
مكتبة React بأن تُعِد مُستمِع لحدث النقر. - عند النقر على الزر، ستستدعي React مُعالِج الحدث
onClick
المُعرَّف في التابعrender()
للمكوّنSquare
. - يستدعي مُعالِج الأحداث هذا
this.props.onClick().
الخاصيّةonClick
الموجودة في المكوّنSquare
مُحدَّدة من قبل المكوّنBoard
. - بما أنّ المكوّن
Board
مرَّر onClick={() => this.handleClick(i)}
إلىSquare
، فسيستدعي هذا الأخيرthis.handleClick(i)
عند النقر عليه. - لم نُعرِّف التابع
handleClick()
حتى الآن، لذا تنهار الشيفرة لدينا.
ملاحظة: تمتلك الخاصيّة onClick
الموجودة في العنصر <button>
معنى مميز بالنسبة لمكتبة React لأنّه مكوّن مُضمَّن في DOM. أمّا بالنسبة للمكوّنات المُخصَّصة مثل Square
فلك حريّة اختيار أسماء الخاصيّات. كان بإمكاننا تسمية الخاصيّة onClick
في المكوّن Square
أو التابع handleClick
في المكوّن Board
بشكلٍ مختلف. على أيّة حال هناك اتفاق في React باستخدام النمط on[Event]
لتسمية الخاصيّة التي تُمثِّل أحداثًا والنمط handle[Event]
لتسمية التوابع التي تُعالِج هذه الأحداث.
عندما نحاول الآن النقر على المربّع يجب أن نحصل على خطأ لأنّنا لم نُعرِّف التابع handleClick
حتى الآن. سنضيف التابع handleClick
إلى صنف المكوّن Board
:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'اللاعب التالي: X';
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
انظر إلى كامل الشيفرة عند هذه النقطة.
بعد هذه التغييرات أصبحنا قادرين على النقر على المربّعات لملئها مع تخزين الحالة ضمن المكوّن Board
بدلًا من وضعها ضمن كل مكوّن مربّع Square
. عندما تتغير حالة المكوّن Board
فستُعيد مكوّنات المربّعات التصيير بشكل تلقائي. سيسمح لك الاحتفاظ بحالة جميع المربعات ضمن المكوّن Board
بتحديد الفائز لاحقًا.
بما أنّ مكوّنات المربعات لم تعد تحتفظ بالحالة فسيستقبل المكوّن Square
القيم من المكوّن Board
ويُخبره عند النقر عليه. بمصطلحات React نستطيع القول عن المكوّن Square
بأنّه مكوّن مضبوط، لأنّ المكوّن Board
أصبح يمتلك كامل التحكم به.
لاحظ كيف أنّنا نستدعي ضمن handleClick
التابع .slice()
لإنشاء نسخة عن المصفوفة squares
لتعديلها بدلًا من تعديل المصفوفة الموجودة. لنشرح لماذا نفعل ذلك في القسم التالي.
لماذا تكون عدم القابلية للتغير هامة؟
اقترحنا في مثال الشيفرة السابق استخدام المُعامِل .slice()
لإنشاء نسخة عن المصفوفة squares
لتعديلها بدلًا من تعديل المصفوفة الموجودة. سنناقش الآن عدم القابلية للتعديل (immutability) وأهمية تعلّمها.
هنالك طريقتان لتغيير البيانات. الطريقة الأولى هي تعديل البيانات مباشرة بتغيير قيمها. والطريقة الثانية هي الحصول على نسخة جديدة من البيانات تمتلك التغييرات المطلوبة ووضعها بدل البيانات الأصليّة.
تغيير البيانات عن طريق تعديلها بشكل مباشر
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// تكون قيمة player الآن
// {score: 2, name: 'Jeff'}
تغيير البيانات بدون تعديلها بشكل مباشر
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// الآن يبقى player بدون تغيير
// ولكن تكون قيمة newPlayer هي {score: 2, name: 'Jeff'}
/// أو إن كنت تستخدم اقتراح صياغة نشر الكائنات تستطيع كتابة
// var newPlayer = {...player, score: 2};
النتيجة النهائيّة هي نفسها ولكن نكسب عدّة فوائد عن طريق عدم تغيير البيانات بشكل مباشر، سنذكر هذه الفوائد الآن.
تبسيط الميزات المعقدة
تجعل عدم قابلية التغيير من الأسهل تنفيذ الميزات المعقدة. لاحقًا في هذا الدليل سنُنفِّذ ميزة السفر عبر الزمن والتي تسمح لنا بمراجعة تاريخ الحركات السابقة في لعبة إكس-أو مع إمكانية القفز إلى الحركات السابقة. هذه الميزة ليست خاصة بالألعاب، فالقدرة على التراجع والعودة عن أفعال محددة هي متطلب شائع في التطبيقات. يسمح لك تجنّب التعديل المباشر للبيانات بالاحتفاظ بإصدارات من تاريخ تحركات اللعبة وإعادة استخدامها لاحقًا.
كشف التغيرات
يصعب كشف التغيّرات في الكائنات القابلة للتعديل لأنّها تُعدَّل بشكل مباشر. يتطلب هذا الكشف مقارنة الكائنات القابلة للتعديل مع النسخ السابقة منها وكامل شجرة الكائنات المطلوبة.
يُعتبَر كشف التغيّرات في الكائنات غير القابلة للتعديل أسهل. إن كان الكائن غير القابل للتعديل مختلفًا عن السابق فنعتبر أنّ الكائن قد تغيّر.
تحديد وقت إعادة التصيير في React
الفائدة الأساسيّة من عدم القابلية للتعديل هي أنّها تساعدك على بناء مكوّنات نقيّة في React. تستطيع البيانات غير القابلة للتعديل أن تُحدِّد بسهولة إذا ما قد أُجريت أي تغييرات، والذي يُساعد بتحديد متى يحتاج المكوّن لإعادة التصيير.
بإمكانك تعلّم المزيد حول التابع shouldComponentUpdate()
وكيفيّة بناء مكوّنات نقية من خلال قراءة توثيق تحسين الأداء.
مكونات الدوال
سنغيّر الآن المكوّن Square
ليُصبِح مكوّن دالّة.
مكوّنات الدوال في React هي طريقة أبسط لكتابة المكوّنات التي تحتوي فقط على تابع التصيير render
بدون أن تمتلك حالتها الخاصّة. فبدلًا من تعريف صنف يمتد إلى الصنف React.Component
نستطيع كتابة دالة تأخذ خاصيّات props
وحقل إدخال وتُعيد ما ينبغي تصييره. من الأسهل كتابة مكوّنات الدوال بدلًا من الأصناف، ويُمكِن التعبير عن الكثير من المكوّنات بهذه الطريقة.
ضع هذه الدالة بدلًا من الصنف Square
:
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
غيّرنا this.props
إلى props
في المرّات التي ظهرت فيها.
انظر إلى كامل الشيفرة عند هذه النقطة.
ملاحظة: عندما عدّلنا المكوّن Square ليصبح مكوّن دالة، فقد غيّرنا أيضًا onClick={() => this.props.onClick()}
إلى الشكل المختصر onClick={props.onClick}
(لاحظ عدم وجود الأقواس على الجانبين). في حال الصنف استخدمنا الدوال السهميّة للوصول إلى قيمة this
الصحيحة، ولكن في مكوّنات الدوال لا حاجة للقلق حول this
.
أخذ الأدوار
نحتاج الآن لإصلاح عيب واضح في لعبة إكس-أو لدينا، فلا يُمكِن وضع الإشارة O
على لوحة اللعبة.
سنُعيِّن أول خطوة لتكون X
افتراضيًّا. نستطيع تعيين هذه القيمة الافتراضيّة عن طريق تعديل الحالة المبدئيّة في الدالة البانية للمكوّن Board
:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
في كل مرة يتحرّك بها اللاعب ستنقلب قيمة المتغيّر xIsNext
(متغير منطقي) لتحديد أي لاعب سيلعب الخطوة التالية وستُحفَظ حالة اللعبة. سنُحدِّث الدالة handleClick
للمكوّن Board
لتقلب قيمة xIsNext
:
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
بها التغيير تستطيع X
و O
أخذ الأدوار. فلنُغيِّر أيضًا نص الحالة في التابع render
للمكوّن Board
بحيث يعرض من هو اللاعب الذي سيلعب الدور التالي:
render() {
const status = 'اللاعب التالي: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// لم تتغير بقية الشيفرة
بعد تطبيق هذه التغييرات يجب أن تملك مكوّن Board
مماثل لما يلي:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'اللاعب التالي: ' + (this.state.xIsNext ? 'X' : 'O');
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</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;
}
سنستدعي التابع calculateWinner(squares)
في تابع التصيير render
للمكوّن Board
للتحقّق من فوز اللاعب. إن فاز اللاعب فنستطيع عرض نص مثل "الفائز: X" أو "الفائز: O". سنضع هذه الشيفرة بدلًا من تصريح الحالة الموجود في التابع render
للمكوّن Board
:
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'الفائز: ' + winner;
} else {
status = 'اللاعب التالي: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
// لم تتغير بقية الشيفرة
نستطيع الآن تغيير الدالة handleClick
للمكوّن Board
لتُعيد قيمتها باكرًا عن طريق تجاهل النقرة إن فاز أحد باللعبة أو إن كان المربّع يحتوي على قيمة مسبقًا:
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
انظر إلى كامل الشيفرة عند هذه النقطة.
تهانينا! تمتلك الآن لعبة إكس-أو تعمل بشكل جيّد. وقد تعلّمت أساسيّات React أيضًا. لذا قد تكون أنت الرابح الحقيقي هنا.
إضافة السفر عبر الزمن
كتمرين أخير فلنجعل من الممكن الرجوع إلى الخلف بالوقت إلى التحركات السابقة في اللعبة.
تخزين تاريخ التحركات
إن عدّلنا المصفوفة squares
سيكون تنفيذ السفر عبر الزمن أمرًا صعبًا.
ولكننا استخدمنا التابع slice()
لإنشاء نسخة من المصفوفة squares
بعد كل تحرّك، والتعامل معها كمصفوفة غير قابلة للتعديل. يسمح لك ذلك بتخزين كل إصدار قديم من هذه المصفوفة، والتنقل بين الأدوار التي حدثت سابقًا.
سنُخزِّن مصفوفات squares
السابقة ضمن مصفوفة أخرى تُدعى history
والتي تُمثِّل حالات لوحة اللعبة من أو إلى آخر تحرّك، ويكون شكلها كما يلي:
history = [
// قبل التحرك الأول
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// بعد التحرك الأول
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// بعد التحرك الثاني
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
نحتاج الآن إلى أن نقرّر أي مكوّن ينبغي أن يمتلك الحالة history
.
رفع الحالة مرّة أخرى
نريد من المكوّن الموجود في أعلى مستوى من اللعبة أن يعرض قائمة بالتحركات السابقة. سيحتاج إلى الوصول إلى الحالة history
لفعل ذلك، لذا سنضعها في المكوّن ذو المستوى الأعلى في اللعبة واسمه Game
.
يُتيح لنا وضع الحالة history
في المكوّن Game
أن نزيل الحالة squares
من المكوّن الابن له وهو Board
. كما رفعنا الحالة من المكوّن Square
إلى Board
، سنرفعها الآن من Board
إلى المكوّن ذو المستوى الأعلى في اللعبة Game
. يُعطي هذا المكوّن Game
التحكّم الكامل ببيانات المكوّن Board
، ويسمح له بأن يأمر المكوّن Board
بتصيير الأدوار السابقة من history
.
سنُعِد في البداية الحالة المبدئيّة للمكوّن Game
ضمن الدالة البانية له:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}