الفرق بين المراجعتين ل"React/tutorial"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
سطر 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>
    );
  }
}

قبل التغييرات:

tictac-empty-1566a4f8490d6b4b1ed36cd2c11fe4b6-a9336.png

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

tictac-numbers-685df774da6da48f451356f33f4be8b2-be875.png

انظر إلى كامل الشيفرة عند هذه النقطة.

تهانينا! لقد مرّرت الآن خاصيّة 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:

  1. سجّل الدخول إلى الموقع أو سجّل في الموقع مع تأكيد بريدك الإلكتروني (مطلوب لتجنّب البريد المزعج spam).
  2. اضغط على الزر "Fork".
  3. اضغط على "Change View" واختر بعدها "Debug mode".
  4. ستملك الآن أدوات التطوير نافذة 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. وهذا ملخّص لكيفية تحقيق ذلك:

  1. تُخبِر الخاصيّة onClick الموجودة في المكوّن <button> مكتبة React بأن تُعِد مُستمِع لحدث النقر.
  2. عند النقر على الزر، ستستدعي React مُعالِج الحدث onClick المُعرَّف في التابع render()‎ للمكوّن Square.
  3. يستدعي مُعالِج الأحداث هذا this.props.onClick()‎. الخاصيّة onClick الموجودة في المكوّن Square مُحدَّدة من قبل المكوّن Board.
  4. بما أنّ المكوّن Board مرَّر ‎onClick={() => this.handleClick(i)}‎ إلى Square، فسيستدعي هذا الأخير ‎this.handleClick(i)‎ عند النقر عليه.
  5. لم نُعرِّف التابع 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>
    );
  }
}