מדריך: מבוא ל-React

מדריך זה לא מחייב שום ידע קודם ב-React.

לפני שאנחנו מתחילים במדריך

<<<<<<< HEAD במהלך מדריך זה אנחנו נבנה משחק קטן. יתכן שתתפתו לדלג עליו מכיוון שאינכם בונים משחקים — אבל תנו לו סיכוי. הטכניקות שתלמדו במדריך הן הבסיס לבניית כל אפליקציית React, ושליטה בהן תיתן לכם הבנה עמוקה של React. ======= We will build a small game during this tutorial. You might be tempted to skip it because you’re not building games — but give it a chance. The techniques you’ll learn in the tutorial are fundamental to building any React app, and mastering it will give you a deep understanding of React.

ed9d73105a93239f94d84c619e84ae8adec43483

טיפ

מדריך זה מיועד לאנשים שמעדיפים ללמוד תוך-כדי עשייה. אם אתם מעדיפים ללמוד קונספט מלמטה למעלה, בדקו את מדריך צעד-אחר-צעד. יתכן שתמצאו את מבוא זה ואת המדריך כמשלימים אחד את השני.

מדריך זה מחולק למספר חלקים:

אין צורך להשלים את כל הסעיפים בבת אחת כדי לקבל את ערך ממדריך זה. נסו להגיע רחוק ככל שתוכלו — גם אם זה רק חלק אחד או שניים.

מה אנחנו בונים?

במדריך זה, אנו נראה כיצד לבנות משחק איקס-עיגול אינטראקטיבי עם React.

תוכלו לראות מה נבנה כאן: תוצאה סופית. אם הקוד אינו עושה לכם הגיון, או אם אינכם מכירים את תחביר הקוד, אל תדאגו! מטרתו של מדריך זה היא לעזור לכם להבין את React ואת התחביר שלה.

אנו ממליצים לכם לבדוק את משחק איקס-עיגול לפני שתמשיך עם הדרכה. אחת התכונות בהן תבחינו היא שיש רשימה ממוספרת בצד ימין של לוח המשחק. רשימה זו נותנת לכם היסטוריה של כל המהלכים שהתרחשו במשחק, והיא מתעדכנת עם התקדמות המשחק.

תוכלו לסגור את משחק האיקס-עיגול ברגע שאתם מבינים אותו. אנו מתחילים מתבנית פשוטה יותר במדריך זה. השלב הבא שלנו הוא לארגן אתכם כך שתוכלו להתחיל לבנות את המשחק.

דרישות קדם

אנחנו יוצאים מנקודת הנחה שיש לכם היכרות כלשהי עם HTML ו-JavaScript, אבל אתם אמורים להיות מסוגלים לעקוב גם אם אתם באים משפת תכנות אחרת. נניח גם שאתם מכירים מושגים מעולם התכנות כמו פונקציות, אובייקטים, מערכים, ובמידה פחותה יותר, מחלקות.

אם עליך להתרענן ב-JavaScript, אנו ממליצים לקרוא את המדריך הבא. שימו לב שאנו משתמשים גם בכמה תכונות מ-ES6 — גרסה חדשה של JavaScript. במדריך זה, אנו משתמשים בפונקציות חץ, מחלקות, והצהרות let, ו-const. אתם יכולים להשתמש ב-Babel REPL כדי לבדוק לאיזה קוד מתקמפל קוד ה-ES6.

הכנה למדריך

ישנן שתי דרכים להשלמת מדריך זה: באפשרותכם לכתוב את הקוד בדפדפן שלכם, או שתוכלו להגדיר סביבת פיתוח מקומית על המחשב שלכם.

אפשרות התקנה 1: כתיבת קוד בדפדפן

זו הדרך המהירה ביותר כדי להתחיל!

תחילה פתחו את קוד ההתחלה הזה בטאב חדש בדפדפן. הטאב החדש אמור להציג לוח משחק איקס-עיגול ריק וקוד React. אנו נערוך את קוד ה-React במדריך זה.

כעת באפשרותך לדלג על אפשרות ההתקנה השנייה וללכת אל הקטע סקירה כללית כדי לקבל סקירה כללית של React.

אפשרות התקנה 2: סביבת פיתוח מקומית

זה אופציונלי לחלוטין ולא נדרש עבור מדריך זה!


אופציונלי: הוראות להתקדמות באמצעות עורך הטקסט המועדף עליכם מקומית

התקנה זו דורשת יותר עבודה, אבל מאפשר לכם להשלים את המדריך באמצעות עורך על פי בחירתכם. הנה השלבים אותם יש לבצע:

  1. וודאו שיש ברשותכם גרסה עדכנית של Node.js מותקנת.
  2. עקבו אחר הוראות ההתקנה ליצירת אפליקציית React כדי ליצור פרוייקט חדש.
npx create-react-app my-app
  1. מחקו את כל הקבצים בתיקיית src/ של הפרוייקט החדש

שימו לב:

אל תמחקו את כל תיקיית src, רק את קבצי המקור המקוריים בתוכה. We’ll החליפו את קבצי המקור המוגדרים כברירת מחדל עם הדוגמאות לפרויקט זה בשלב הבא.

cd my-app
cd src

# אם אתם משמשים במק או לינוקס:
rm -f *

# או, אם אתם משתמשים בווינדוס:
del *

# אז, חזרו לתיקיית הפרוייקט
cd ..
  1. הוסיפו את הקובץ שנקרא index.css בתקיית src/ עם קוד ה-CSS הזה.

  2. הוסיפו את הקובץ שנקרא index.js בתקיית src/ עם קוד ה-JS הזה.

  3. הוסיפו את השורות הבאות בתחילת הקובץ index.js בתיקיית src/:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

כעת אם תריצו npm start בתיקיית הפרוייקט ותפתחו את http://localhost:3000 בדפדפן, אתם אמורים לראות לוח איקס-עיגול ריק.

אנו ממליצים לעקוב אחר ההוראות האלו כדי להגדיר הדגשת תחביר עבור העורך שלך.

הצילו, אני תקוע!

אם אתם נתקעים, בדקו את משאבי התמיכה בקהילה. בפרט, צ’אט Reactiflux הוא דרך מצוינת לקבל עזרה במהירות. אם אתם לא מקבלים תשובה, או אם נשארתם תקועים, אנא שלחו לנו את הבעיה בה נתקלתם, ואנו נעזור לכם.

סקירה כללית

כעת שאתם מוכנים, בואו נקבל סקירה כללית של React!

מה זה React?

React היא ספריית JavaScript הצהרתית, יעילה וגמישה של לבניית ממשקי משתמש. היא מאפשרת לייצר ממשקי משתמש מורכבים מחתיכות קטנות ומבודדות של קוד בשם קומפוננטות (“components”).

ל-React יש מספר קומפוננטות שונות, אבל נתחיל מתת-המחלקה React.Component

class ShoppingList extends React.Component {
  render() {
    return (
      <div className="shopping-list">
        <h1>Shopping List for {this.props.name}</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
          <li>Oculus</li>
        </ul>
      </div>
    );
  }
}

// Example usage: <ShoppingList name="Mark" />

נגיע לתגיות המצחיקות שמזכירות XML בהמשך. אנו משתמשים בקומפוננטות כדי לומר ל-React מה אנחנו רוצים לראות על המסך. כאשר הנתונים שלנו ישתנו, React יעדכן וירנדר מחדש את הקומפוננטות שלנו.

כאן, ShoppingList היא מחלקת קומפוננטת React, או מסוג קומפוננטת React. קומפוננטה לוקחת פרמטרים, הנקראים props (פרופס, קיצור עבור “מאפיינים”, properties), ומחזיר היררכיה של תצוגות (views) להצגה דרך המתודה render.

המתודה render מחזירה תיאור של מה שאתם רוצים לראות על המסך. React לוקחת את התיאור ומציגה את התוצאה. בפרט, render מחזירה אלמנט React, שהוא תיאור מופשט של מה שצריך לרנדר. רוב מפתחי React משתמשים בתחביר מיוחד בשם “JSX” שהופך את המבנים האלה לקלים יותר לכתיבה. התחביר <div /> משתנה בזמן הבנייה (build time) ל-React.createElement('div'). הדוגמה שלמעלה שקולה לקוד:

return React.createElement('div', {className: 'shopping-list'},
  React.createElement('h1', /* ... h1 children ... */),
  React.createElement('ul', /* ... ul children ... */)
);

ראו גרסה מורחבת מלאה.

אם אתם סקרנים, createElement() מתוארת ביתר פירוט במסמך ה-API, אך לא נשתמש בה במדריך זה. במקום זאת, נמשיך להשתמש ב-JSX.

JSX מגיעה עם מלוא העוצמה של JavaScript. תוכלו לשים כל ביטוי JavaScript בין סוגריים מסולסלים בתוך JSX. כל אלמנט React הוא אובייקט JavaScript שניתן לאחסן במשתנה או להעביר בתוך התוכנית שלך.

הקומפוננטה ShoppingList למעלה רק מרנדרת קומפוננטות מובנות ב-DOM כמו <div /> ו-<li />. אבל ניתן לבנות ולרנדר קומפוננטות React מותאמות אישית באותו אופן. לדוגמה, כעת אנו יכולים להתייחס לקומפוננטת רשימת הקניות כולה על ידי כתיבת <ShoppingList />. כל קומפוננטת React היא מוכמסת (encapsulated) והיא יכולה לפעול באופן עצמאי; זה מאפשר לנו לבנות ממשקי משתמש מורכבים מקומפוננטות פשוטות.

בדיקת קוד הבסיס

אם אתם מתכוונים לעבוד על המדריך בדפדפן שלכם, פתחו את הקוד הבא בטאב חדש: קוד בסיס. אם אתם הולכים לעבוד על המדריך מקומית, במקום זאת פתחו את src/index.js בתיקייה הפרויקט שלכם (כבר נגעתם בקובץ זה במהלך ההתקנה).

קוד בסיס זה הוא הבסיס למה שאנחנו בונים. סיפקנו את קוד הסגנון (CSS) כך שאתם צריכים להתמקד רק בלמידת React ותכנות משחק האיקס-עיגול.

בעת בדיקת הקוד, תבחינו שיש לנו שלוש קומפוננטות React:

  • ריבוע (Square)
  • לוח (Board)
  • משחק (Game)

קומפוננטת הריבוע מרנדרת <button> (כפתור) אחד והלוח מציג 9 ריבועים. קומפוננטת המשחק מרנדרת לוח עם ערכי שומרי מקום, שאותם נשנה מאוחר יותר. אין כרגע קומפוננטות אינטראקטיביות.

העברת נתונים באמצעות Props

כדי ללכלך את הידיים, בואו ננסה להעביר כמה נתונים מתוך קומפוננטת הלוח שלנו לקומפוננטת הריבוע שלנו.

אנו ממליצים בחום להקליד את הקוד ביד בזמן שאתם עוברים על המדריך ולא באמצעות העתק/הדבק. זה יעזור לכם לפתח זיכרון שריר והבנה חזקה יותר.

במתודת renderSquare של הלוח, שנו את הקוד על מנת להעביר את הערך שנקרא value לריבוע:

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }

שנו את מתודת render של ריבוע כדי להציג את הערך על-ידי החלפת {/* TODO */} עם {this.props.value}:

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

לפני:

React Devtools

אחרי: אתם אמורים לראות מספר התוך כל אחד מהריבועים בפלט המוצג.

React Devtools

צפו בקוד המלא עד נקודה זו

מזל טוב! בדיוק “העברתם prop” מקומפוננטת האב לוח לקומפוננטת הבן ריבוע. העברת props היא הדרך בה זורם מידע באפליקציות React, מהורים לילדיהם.

יצירת קומפוננטה אינטראקטיבית

בואו נמלא את הקומפוננטה ריבוע עם “X” כאשר אנו לוחצים עליה. ראשית, שנו את תג הכפתור שמוחזר מפונקצית render() של קומפוננטת הריבוע לזה:

class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={function() { alert('click'); }}>
        {this.props.value}
      </button>
    );
  }
}

אם תלחצו על ריבוע עכשיו, אתם אמורים לקבל התראה מהדפדפן שלכם.

שימו לב

כדי לחסוך בהקלדות ולהמנע מהתנהגות מבלבלת של this, אנחנו נשתמש בתחביר פונקציית חץ עבור פונקציות מנהלי אירועים מכאן והלאה:

class Square extends React.Component {
 render() {
   return (
     <button className="square" onClick={() => alert('click')}>
       {this.props.value}
     </button>
   );
 }
}

שימו לב איך עם onClick={() => alert('click')}, אנו מעבירים פונקציה בתור ה-prop onClick. React תקרא לפונקציה זו רק אחרי לחיצה. לשכוח את () => ולכתוב רק onClick={alert('click')} היא טעות נפוצה, והיא תגרום להקפצת ההתראה בכל פעם שהקומפוננטה מתרנדרת מחדש.

בצעד הבא, אנחנו רוצים שהקומפוננטה ריבוע “תזכור” שהיא נלחצה, ותמלא את עצמה עם הסימן “X”. כדי “לזכור” דברים, קומפוננטות משתמשות ב-state (מצב).

קומפוננטות React יכולות להשתמש ב-state על ידי הגדרת this.state בבנאים (constructor) שלהם. this.state צריך להיחשב כפרטי לקומפוננטת ה-React שבה הוא מוגדר. בואו נאחסן את הערך הנוכחי של הריבוע ב-this.state, ונשנה אותו כאשר נלחץ על הריבוע.

תחילה, נוסיף בנאי למחלקה כדי לאתחל את ה-state.

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

שימו לב

ב-מחלקות של JavaScript, עליכם תמיד לקרוא לפונקציה super בעת הגדרת בנאי של תת-מחלקה. בכל מחלקה של קומפוננטת React שיש לה constructor עליו להתחיל עם קריאה ל-super(props).

כעת נשנה את מתודת render של ריבוע כך שתציג את הערך שמוגדר ב-state בעת לחיצה:

  • החליפו את this.props.value עם this.state.value בתוך התגית <button>.
  • החליפו את מנהל האירוע onClick={...} עם onClick={() => this.setState({value: 'X'})}.
  • שימו את ה-props className ו-onClick בשורות נפרדות על מנת שתהיה לנו קריאות טובה יותר.

לאחר ביצוע שינויים אלה, תגית הכפתור <button> שמוחזרת ממתודת render של ריבוע אמורה להיראות כך:

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 במתודה render של ריבוע, אנו אומרים ל-React לרנדר מחדש את ריבוע זה בכל פעם שהכפתור <button> שלו נלחץ. לאחר העדכון, הערך this.state.value של הריבוע יהיה 'X', כך שנראה את ה-X על לוח המשחק. אם תלחצו על כל אחד מהריבועים, X אמור להופיע.

כאשר אנו קוראים ל-setState בקומפוננטה כלשהי, React מעדכנת אוטומטית גם את קומפוננטות הבנים בתוכה.

צפו בקוד המלא עד נקודה זו

כלי פיתוח

התוסף React Devtools עבור Chrome ו-Firefox מאפשר לכם לבדוק עץ קומפוננטות של React עם כלי הפיתוח של הדפדפן שלכם.

React Devtools

ה-React DevTools מאפשר לכם לבדוק את ה-props וה-state של קומפוננטות ה-React שלכם.

לאחר התקנת React DevTools, באפשרותכם ללחוץ לחיצה ימנית על כל אלמנט בדף, ללחוץ על “בדוק” (“Inspect”) כדי לפתוח את כלי הפיתוח, והכרטיסייה React תופיע ככרטיסייה האחרונה מימין.

עם זאת, שימו לב כי ישנם כמה צעדים נוספים כדי לגרום לזה לעבוד עם CodePen:

  1. היכנסו או הירשמו ואשרו את הדוא”ל שלכם (נדרש כדי למנוע דואר זבל).
  2. לחצו על הלחצן “פצל” (“Fork”).
  3. לחצו על “שנה תצוגה” (“Change View”) ולאחר מכן בחרו “מצב דיבאג” (“Debug mode”).
  4. בכרטיסייה החדשה שנפתחת, ל-devtools כעת אמורה להיות הכרטיסייה React.

השלמת המשחק

עכשיו יש לנו את אבני הבניין הבסיסיים שלנו למשחק האיקס-עיגול. כדי לקבל משחק שלם, עכשיו אנחנו צריכים להחליף לסירוגין בין השמת “X”-ים ו-”O”-ים על הלוח, ואנחנו צריכים דרך כדי לקבוע את הזוכה.

הרמת ה-State למעלה

נכון לעכשיו, כל קומפוננטת ריבוע שומרת על מצב המשחק. כדי לבדוק מי הזוכה, נשמור על הערך של כל אחד מ-9 הריבועים במקום אחד.

אנחנו יכולים לחשוב כי הלוח צריך רק לשאול כל ריבוע על ה-state שלו. למרות שגישה זו אפשרית ב-React, אנו נרתעים ממנה משום ששימוש בה הופך את הקוד להיות קשה להבנה, רגיש לבאגים וקשה לשכתוב. במקום זאת, הגישה הטובה יותר היא לאחסן את ה-state של המשחק בקומפוננטת האב לוח במקום בכל ריבוע. קומפוננטת הלוח יכולה להגיד לכל ריבוע מה להציג על ידי העברת prop, בדיוק כמו שעשינו כאשר העברנו מספר לכל ריבוע.

כדי לאסוף נתונים ממספר ילדים, או כדי לאפשר לשני קומפוננטות ילדים לתקשר אחת עם השניה, עלינו להכריז על state משותף בקומפוננטת האב שלהם במקום. קומפוננטת האב יכולה להעביר את ה-state שלה בחזרה לילדים באמצעות שימוש ב-props; פעולה זו שומרת על קומפוננטות הילדים מסונכרנות זו עם זו ועם קומפוננטת האב.

הרמת ה-state לקומפוננטת האב היא פעולה נפוצה כאשר משכתבים קומפוננטות React — בואו ניקח הזדמנות זו כדי לנסות זאת.

הוסיפו בנאי ללוח וקבעו את ה-state הראשוני של הלוח כך שיכיל מערך עם 9 ערכים ריקים (nulls) התואמים ל-9 הריבועים:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  renderSquare(i) {
    return <Square value={i} />;
  }

כאשר נמלא את הלוח בשלב מאוחר יותר, המערך this.state.squares ייראה כמו משהו כזה:

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

מתודת “renderSquare” של הלוח כרגע נראית כך:

  renderSquare(i) {
    return <Square value={i} />;
  }

בתחילה, העברנו את props ה-value למטה מהלוח כדי להראות מספרים מ-0 ועד 8 בכל ריבוע. בשלב קודם שונה, החלפנו את המספרים עם סימן “X” שנקבע על ידי ה-state של ריבוע עצמו. זו הסיבה שריבוע מתעלם כעת מהערך value שהלוח מעביר אליו.

כעת נשתמש במנגנון העברת ה-props שוב. אנו נשנה את הלוח כדי להורות לכל ריבוע בן באופן אינדיבידואלי על הערך הנוכחי שלו ("X", "O", או null). כבר הגדרנו את מערך הריבועים squares בבנאי הלוח, ואנו נשנה את מתודת renderSquare של הלוח כדי לקרוא ממנו:

  renderSquare(i) {
    return <Square value={this.state.squares[i]} />;
  }

צפו בקוד המלא עד נקודה זו

כל ריבוע יקבל כעת props value אשר יהיה אחד מן הערכים 'X', 'O', או null עבור ריבועים ריקים.

כעת, אנחנו צריכים לשנות את מה שקורה כאשר ריבוע כלשהו נלחץ. קומפוננטת הלוח עכשיו שומרת אילו ריבועים מולאו. אנחנו צריכים ליצור דרך לריבוע לעדכן את ה-state של הלוח. מאחר שה-state נחשב פרטי לקומפוננטה המגדירה אותו, אין באפשרותנו לעדכן את ה-state של הלוח ישירות מהריבוע.

במקום זאת, נעביר למטה פונקציה מהלוח לריבוע, ונדאג לכך שריבוע יקרא לפונקציה זו כאשר הריבוע נלחץ. נשנה את המתודה renderSquare בלוח למתודה הבאה:

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

שימו לב

אנו מחלקים את האלמנט שהוחזר למספר שורות לשיפור הקריאות, ומוסיפים סוגריים כדי ש-JavaScript לא תוסיף נקודה-פסיק לאחר ה-return ותשבור את הקוד שלנו.

עכשיו אנחנו מעבירים למטה שני props מהלוח לריבוע: value ו-onClick. ה-props onClick היא פונקציה שאליה יכול ריבוע לקרוא כאשר לוחצים עליו. נערוך את השינויים הבאים בריבוע:

  • נחליף את this.state.value עם this.props.value במתודת render של ריבוע
  • נחליף את this.setState() עם this.props.onClick() במתודת render של ריבוע
  • נמחק את הבנאי constructor מריבוע מכיון שריבוע כבר לא עוקב אחר ה-state של המשחק

לאחר ביצוע שינויים אלו, קומפוננטת הריבוע נראה כך:

class Square extends React.Component {
  render() {
    return (
      <button
        className="square"
        onClick={() => this.props.onClick()}
      >
        {this.props.value}
      </button>
    );
  }
}

כאשר לוחצים על ריבוע, נקראת פונקציית onClick שמסופקת על ידי הלוח. הנה סקירה של איך התנהגות זו מושגת:

  1. ה-prop onClick בקומפוננטה המובנה של ה-DOM <button> גורם ל-React להגדיר מאזין לאירועי לחיצות.
  2. כאשר לוחצים על הכפתור, React יקרא למטפל האירועים onClick המוגדר במתודה render() של ריבוע.
  3. מטפל אירוע זה קורא ל-this.props.onClick(). ה-props onClick של ריבוע הוגדר על ידי הלוח.
  4. מאחר שהלוח העביר את onClick={() => this.handleClick(i)} לריבוע, הריבוע קורא ל-this.handleClick(i) בעת לחיצה עליו.
  5. עדיין לא הגדרנו את המתודה handleClick(), ולכן שהקוד שלנו קורס. אם תלחצו על ריבוע עכשיו, אתם אמורים לראות מסך שגיאה אדום שאומר משהו כמו “this.handleClick is not a function”.

שימו לב

לתכונה onClick של אלמנט <button> של ה-DOM יש משמעות מיוחדת עבור React מכיוון שהיא קומפוננטה מובנה. עבור קומפוננטות מותאמות אישית כמו ריבוע, הגדרת השמות תלויה בנו. אנו יכולים להגדיר כל שם ל-prop onClick של ריבוע או למתודת handleClick של לוח, והקוד יעבוד באותו אופן. ב-React, זוהי קונבנציה להשתמש בשמות כמו on[Event] עבור props אשר מייצגים אירועים ו-handle[Event] עבור המתודות אשר מטפלות באירועים.

כאשר אנו מנסים ללחוץ על ריבוע, אנחנו אמורים לקבל שגיאה כי עדיין לא הגדרנו את handleClick. כעת נוסיף את handleClick למחלקה לוח:

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 = 'Next player: 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>
    );
  }
}

צפו בקוד המלא עד נקודה זו

לאחר ביצוע שינויים אלה, נוכל שוב ללחוץ על הריבועים כדי למלא אותם, כפי שיכלנו קודם. למרות זאת, כעת ה-state מאוחסן בקומפוננטת הלוח במקום בקומפוננטות הריבועים עצמם. כאשר ה-state של הלוח משתנה, קומפוננטות הריבוע מרונדרות מחדש באופן אוטומטי. שמירת מצב כל הריבועים בקומפוננטת הלוח תאפשר לו לקבוע את הזוכה בעתיד.

מאחר שקומפוננטות הריבוע אינן מתחזקות state יותר, קומפוננטות הריבוע מקבלות ערכים מקומפוננטת הלוח ומעדכנות את קומפוננטת הלוח כאשר לוחצים עליהן. במונחי React, קומפוננטות הריבוע הן כעת קומפוננטות מבוקרות. הלוח הוא בעל שליטה מלאה עליהן.

שימו לב כיצד ב-handleClick אנו קוראים ל-.slice() כדי לייצר עותק של מערך squares על מנת לשנותו במקום לשנות את המערך הקיים. נסביר מדוע אנו יוצרים עותק של מערך squares בחלק הבא.

מהי החשיבות של אי-יכולת השתנות

בדוגמת הקוד הקודמת, הצענו להשתמש במתודה .slice() כדי ליצור עותק של מערך squares על מנת לשנותו במקום לשנות את המערך הקיים. כעת נדון באי-יכולת השתנות (Immutability) ומדוע חשוב ללמוד על אי-יכולת השתנות.

יש בדרך כלל שתי גישות לשינוי נתונים. הגישה הראשונה היא לשנות את הנתונים על ידי שינוי ישיר של ערכי הנתונים. הגישה השנייה היא להחליף את הנתונים עם עותק חדש שבו יש את השינויים הרצויים.

שינוי נתונים עם מוטציה

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}

שינוי נתונים ללא מוטציה

var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}

// Or if you are using object spread syntax proposal, you can write:
// var newPlayer = {...player, score: 2};

התוצאה הסופית היא זהה, אך על ידי העתקה ללא מוטציה (או שינוי הנתונים הבסיסיים) ישירות, אנו מרוויחים מספר יתרונות המתוארים בהמשך.

פיצ’רים מורכבים הופכים לפשוטים

אי-יכולת השתנות הופכת פיצ’רים מורכבים להרבה יותר קלים ליישום. מאוחר יותר במדריך זה, נוכל ליישם פיצ’ר של “נסיעה בזמן” (“time travel”) המאפשר לנו לסקור את היסטוריית משחק האיקס-עיגול ו”לקפוץ בחזרה” למהלכים קודמים במשחק. פונקציונליות זו אינה ספציפית למשחקים — היכולת לבטל ולבצע מחדש פעולות מסוימות היא דרישה נפוצה בתוכנות. הימנעות ממוטציה ישירה של נתונים מאפשרת לנו לשמור על גירסאות קודמות של היסטוריית המשחק ללא שינוי, ולהשתמש בהן שוב במועד מאוחר יותר.

זיהוי שינויים

זיהוי שינויים באובייקטים משתנים היא קשה מכיוון שהם משתנים ישירות. זיהוי זה מחייב את האובייקט המשתנה להיות מושווה לעותקים קודמים של עצמו ומעבר על עץ האובייקט כולו.

זיהוי שינויים באובייקטים בלתי ניתנים לשינוי הוא הרבה יותר קל. אם האובייקט הבלתי משתנה שאליו אנחנו מתייחסים שונה מהקודם, אזי האובייקט השתנה.

ההחלטה מתי לרנדר מחדש ב-React

היתרון העיקרי של אי-יכולת השתנות הוא שהיא עוזר לנו לבנות קומפוננטות טהורות (pure components) ב-React. נתונים בלתי ניתנים לשינוי מאפשרים לקבוע בקלות אם בוצעו שינויים, דבר אשר מסייע לקבוע מתי קומפוננטה דורשת רינדור מחדש.

אתם יכולים ללמוד עוד על shouldComponentUpdate() וכיצד ניתן לבנות קומפוננטות טהורות על ידי קריאת אופטימיזציה של ביצועים.

קומפוננטות פונקציה

כעת נשנה את הריבוע כך שיהיה קומפוננטת פונקציה (function component).

ב-React, קומפוננטות פונקציה הן דרך פשוטה יותר לכתוב קומפוננטות המכילות רק מתודת render ואין להן state משלהן. במקום להגדיר מחלקה המרחיבה את React.Component, אנו יכולים לכתוב פונקציה שמקבלת props כקלט ומחזירה את מה שצריך להיות מרונדר. קומפוננטות פונקציה הן פחות מייגעות לכתיבה מאשר מחלקות, וקומפוננטות רבות יכולות לבוא לידי ביטוי בדרך זו.

החליפו את מחלקת ריבוע בפונקציה הבאה:

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

החלפנו את this.props ב-props בשתי הפעמים שהוא מופיע.

צפו בקוד המלא עד נקודה זו

שימו לב

כאשר שינינו את הריבוע והפכנו אותו לקומפוננטת פונקציה, שינינו גם את onClick={() => this.props.onClick()} לגירסה קצרה יותר onClick={props.onClick} (שימו לב לחיסרון בסוגריים משני הצדדים).

חלוקה לתורות

עכשיו אנחנו צריכים לתקן פגם ברור במשחק האיקס-עיגול שלנו: ה-”O”ים לא יכולים להיות מסומנים על הלוח.

אנו נקבע את המהלך הראשון להיות “X” כברירת מחדל. אנו יכולים להגדיר את ברירת המחדל הזו על ידי שינוי ה-state ההתחלתי בבנאי הלוח שלנו:

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

בכל פעם ששחקן מבצע מהלך, xIsNext (בוליאני) יתהפך כדי לקבוע איזה שחקן משחק בתור הבא ומצב המשחק יישמר. אנו נעדכן את הפונקציה handleClick של הלוח כדי להפוך את הערך של 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”ים יכולים להתחלף בתורות. נסו זאת!

בואו נשנה גם את טקסט שורת המצב (ה-”status”) בפונקציית render של הלוח כך שתציג איזה שחקן משחק את התור הבא:

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      // ההמשך לא השתנה

לאחר החלת שינויים אלה, קומפוננטת הלוח שלכם אמורה להיראות כך:

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 = 'Next player: ' + (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;
}

בהינתן מערך של 9 ריבועים, פונקציה זו תבדוק מי הזוכה ותחזיר 'X', 'O', או null בהתאמה.

נקרא לפונקציה calculateWinner(squares) מתוך הפונקציה render של הלוח כדי לבדוק אם שחקן זכה. אם שחקן זכה, אנו יכולים להציג טקסט כגון “הזוכה: X” או “הזוכה: O”. אנו מחליפים את הצהרת “שורת המצב” (status) בפונקציה render של הלוח באמצעות קוד זה:

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      // the rest has not changed

כעת אנו יכולים לשנות את הפונקציה handleClick של הלוח כדי שתחזור מהר יותר על ידי התעלמות מקליק אם מישהו זכה במשחק או אם ריבוע כבר מלא:

  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. אז אתם כנראה המנצחים האמיתיים כאן.

הוספת מסע בזמן

בתור תרגיל אחרון, בואו נאפשר “לחזור אחורה בזמן” (time travel) למהלכים הקודמים במשחק.

אחסון היסטוריה של מהלכים

אם היינו משנים ערכים במערך squares, יישום הנסיעה בזמן היה קשה מאוד.

לעומת זאת, מאחר שהשתמשנו ב-slice() כדי ליצור עותק חדש של מערך הריבועים squares לאחר כל מהלך, והתייחסנו אליו כבלתי ניתן לשינוי. הדבר יאפשר לנו לאחסן כל גרסה קודמת של מערך הריבועים squares, ולנווט בין התורות שכבר התרחשו.

נשמור את מערכי הריבועים squares הקודמים במערך אחר שלו נקרא היסטוריה (history). מערך ההיסטוריה מייצג את כל מצבי הלוח, מהצעד הראשון ועד האחרון, ויש לו צורה כזאת:

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

עכשיו אנחנו צריכים להחליט איזה קומפוננטה צריכה להיות הבעלים של הסטוריית המצב (history).

הרמת ה-State למעלה, שוב

נרצה שקומפוננטת המשחק ברמה העליונה ביותר תציג רשימה של מהלכים קודמים. היא תזדקק לגישה למשתנה ההיסטוריה history כדי לעשות זאת, לכן נציב את היסטוריית המצבים history בקומפוננטת המשחק ברמה העליונה.

הצבת מצב ההיסטוריה history בקומפוננטת המשחק מאפשרת לנו להסיר את מצב הריבועים squares מהבן שלה, קומפוננטת הלוח. בדיוק כמו ש“הרמנו את ה-state למעלה” מקומפוננטת הריבוע לתוך קומפוננטת הלוח, עכשיו נרים אותו מהלוח לתוך הרמה העליונה שהיא קומפוננטת המשחק. זה נותן לקומפוננטת המשחק שליטה מלאה בנתוני הלוח, ומאפשר לה להנחות את הלוח לרנדר תורים קודמים ממתוך ההיסטוריה history.

ראשית, עלינו להגדיר את ה-state הראשוני של קומפוננטת המשחק בתוך הבנאי שלה:

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>
    );
  }
}

הדבר הבא שיהיה עלינו לעשות הוא לדאוג שקומפוננטת הלוח תקבל את props הריבועים (squares) ו-onClick מקומפוננטת המשחק. מכיוון שיש לנו כעת מנהל אירוע לחיצה יחיד בלוח עבור מספר רב של ריבועים, נצטרך להעביר את המיקום של כל ריבוע לתוך מנהל האירוע onClick כדי לציין איזה ריבוע נלחץ. לפניכם השלבים הנדרשים כדי לשנות את קומפוננטת הלוח:

  • מחיקת הבנאי constructor מהלוח.
  • החלפת this.state.squares[i] ב-this.props.squares[i] בפונקציית renderSquare של הלוח.
  • החלפת this.handleClick(i) ב-this.props.onClick(i) בפונקציית renderSquare של הלוח.

קומפוננטת הלוח נראית עכשיו כך:

class Board extends React.Component {
  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,
    });
  }

  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (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>
    );
  }
}

אנו נעדכן את הפונקציה render של קומפוננטת המשחק כך שתשתמש בערך ההיסטוריה העדכני ביותר כדי לקבוע ולהציג את מצב המשחק:

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }

מאחר שקומפוננטת המשחק מרנדרת עכשיו את מצב המשחק, אנו יכולים להסיר את הקוד התואם ממתודת render של הלוח. לאחר שכתוב הקוד, הפונקציה render של הלוח נראית כך:

  render() {
    return (
      <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>
    );
  }

לסיום, אנחנו צריכים להעביר את המתודה handleClick מקומפוננטת הלוח אל קומפוננטת המשחק. אנחנו צריכים גם לשנות את handleClick כי ה-state של קומפוננטת המשחק הוא בעל מבנה בצורה שונה. בתוך מתודת handleClick של המשחק, אנו משרשרים ערכי היסטוריה חדשים לתוך ההיסטוריה history.

  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares,
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

שימו לב

שלא כמו המתודה push() של מערך שיתכן שאתם מכירים טוב יותר, המתודה concat() אינה משנה את המערך המקורי, לכן אנו מעדיפים אותה.

בשלב זה, קומפוננטת הלוח צריכה רק את מתודות ה-renderSquare ו-render. ה-state של המשחק והמתודה handleClick צריכים להיות בקומפוננטת המשחק.

צפו בקוד המלא עד נקודה זו

הצגת המהלכים הקודמים

מכיוון שאנו מקליטים את ההיסטוריה של משחק האיקס-עיגול, אנו יכולים כעת להציג אותה לשחקן כרשימה של מהלכים קודמים.

למדנו מוקדם יותר כי קומפוננטות React הן אובייקטי JavaScript מדרגה ראשונה; אנחנו יכולים להעביר אותם בין מחלקות ופונקציות ביישומים שלנו. כדי לרנדר פריטים מרובים ב- React, אנו יכולים להשתמש במערך של קומפוננטות React.

ב-JavaScript, למערכים יש את מתודת map() אשר נעשה בה שימוש לעתים קרובות כדי למפות מידע למידע אחר, למשל:

const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

על ידי שימוש במתודה map, אנו יכולים למפות את היסטוריית המהלכים שלנו לקומפוננטות React המייצגות לחצנים על המסך, ולהציג רשימה של לחצנים כדי “לקפוץ” למהלכים קודמים.

בואו נמפה בעזרת map את ההיסטוריה history במתודה render של המשחק:

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }

צפו בקוד המלא עד נקודה זו

עבור כל מהלך בהיסטוריית משחק האיקס-עיגול, אנו יוצרים פריט רשימה <li> המכיל כפתור <button>. לכפתור יש מנהל אירוע onClick אשר קורא למתודה הנקראת this.jumpTo(). לא יישמנו את המתודה jumpTo() עדיין. לעת עתה, אנחנו צריכים לראות רשימה של המהלכים שהתרחשו במשחק ואזהרה במסוף כלי הפיתוח (developer tools console) שאומרת:

Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.

תרגום:

אזהרה: כל ילד במערך או איטרטור צריך להיות בעל “מפתח” ייחודי. בדוק את מתודת רנדר של “משחק”.

בואו נדבר על משמעות האזהרה למעלה.

בחירת מפתח

כאשר אנו מרנדרים רשימה, React מאחסן מידע על כל פריט רשימה שרונדר. כאשר אנו מעדכנים רשימה, React צריכה לקבוע מה השתנה. יכולנו להוסיף, להסיר, לסדר מחדש או לעדכן את הפריטים ברשימה.

תארו לעצמכם מעבר ממצב

<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>

למצב

<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>

בנוסף לספירות המעודכנות, בן-אדם שקורא קוד זה בוודאי יגיד שהחלפנו את הסדר של אלקסה ובן והכניסנו את קלאודיה בין אלקסה ובן. לעומת זאת, React היא תוכנת מחשב ואינה יודעת מה התכוונו. מכיוון ש-React אינה יכולה לדעת את כוונותינו, אנו צריכים לציין props מפתח (key) עבור כל פריט ברשימה כדי להבדיל כל פריט רשימה מהאחים שלו. אפשרות אחת היא להשתמש במחרוזות alexa, ben, claudia. אם היינו מציגים נתונים ממסד נתונים, היינו יכולים להשתמש במזהי מסד הנתונים של Alexa, Ben ו-Claudia.

<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>

כאשר רשימה מרונדרת מחדש, React לוקחת כל מפתח של פריט ברשימה ומחפשת עבור מפתח תואם בפריטים ברשימה הקודמת. אם הרשימה הנוכחית כוללת מפתח שלא היה קיים קודם לכן, React יוצרת קומפוננטה חדשה. אם ברשימה הנוכחית חסר מפתח שהיה קיים ברשימה הקודמת, React משמידה את הקומפוננטה הקודמת. אם שני מפתחות תואמים, הקומפוננטה המתאימה מועברת. מפתחות מספרים ל-React על הזהות של כל קומפוננטה, דבר המאפשר ל-React לשמור על ה-state בין רינדורים מחדש. אם מפתח של קומפוננטה משתנה, הקומפוננטה תושמד ותיווצר מחדש עם state חדש.

ה-props key (מפתח) הוא props מיוחד ושמור ב- React (יחד עם ref, תכונה מתקדמת יותר). כאשר נוצר אלמנט, React מחלץ את ה-props key ומאחסן את המפתח ישירות על האלמנט המוחזר. למרות ש-key נראה כאילו הוא שייך ל-props, לא ניתן לפנות אל key באמצעות this.props.key. React משתמשת אוטומטית ב-key כדי להחליט אילו קומפוננטות לעדכן. קומפוננטה לא תוכל לתשאל על ה-key שלה.

מומלץ מאוד להקצות מפתחות הולמים בכל פעם שאתם בונה רשימות דינמיות. אם אין לכם מפתח הולם, מומלץ לשקול ארגון מחדש של הנתונים שלכם כך שיהיו לכם כאלו.

אם לא צוין מפתח, React תציג אזהרה ותשתמש באינדקס של המערך כמפתח כברירת מחדל. שימוש באינדקס המערך כמפתח הוא בעייתי בעת ניסיון לבצע סידור מחדש של פריטי רשימה או הוספה/הסרה של פריטי רשימה. הגדרת key={i} במפורש משתיקה את האזהרה אבל משאירה את אותן בעיות כמו אינדקסים של מערך והיא לא מומלצת ברוב המקרים.

מפתחות לא צריכים להיות ייחודיים גלובלית; הם רק צריכים להיות ייחודיים בין קומפוננטות לבין האחים שלהן.

מימוש מסע בזמן

בהיסטוריה של משחק האיקס-עיגול, לכל מהלך קודם יש מזהה ייחודי הקשור אליו: זהו המספר הסידורי של המהלך. המהלכים לעולם אינם מסודרים מחדש, נמחקים, או מוכנסים באמצע, לכן בטוח להשתמש באינדקס המהלך כמפתח.

במתודת render של קומפוננטת המשחק, נוכל להוסיף את המפתח כ-<li key={move}> והאזהרה של React בנוגע למפתחות אמורה להיעלם:

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

צפו בקוד המלא עד נקודה זו

לחיצה על כל אחד מכפתורי פריטי הרשימה זורקת שגיאה מכיוון שמתודת jumpTo אינה מוגדרת. לפני שאנו מיישמים את jumpTo, נוסיף את מספר התור stepNumber ל-state של קומפוננטת המשחק כדי לציין באיזה צעד אנו צופים כעת.

ראשית, הוסיפו את stepNumber: 0 ל-state ההתחלתי בבנאי של המשחק:

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

בשלב הבא, נגדיר את מתודת jumpTo במשחק כדי לעדכון את מספר הצעד stepNumber. בנוסף נקבע את xIsNext ל-true אם המספר שאנו משנים את stepNumber להיות הוא זוגי:

  handleClick(i) {
    // המתודה לא השתנתה
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    // המתודה לא השתנתה
  }

כעת נערוך מספר שינויים במתודת handleClick של המשחק אשר נקראת כאשר השחקן לוחץ על ריבוע.

מצב stepNumber שהוספנו משקף את המהלך המוצג למשתמש כעת. לאחר שנעשה מהלך חדש, עלינו לעדכן את stepNumber על-ידי הוספת stepNumber: history.length כחלק מהארגומנטים של this.setState. זה מבטיח שאנחנו לא נתקע כשאנחנו מראים את אותו מהלך אחרי שמהלך חדש כבר בוצע.

בנוסף נחליף את הקריאה מ-this.state.history עם this.state.history.slice(0, this.state.stepNumber + 1). זה מבטיח שאם אנחנו “חוזרים אחורה בזמן” ולאחר מכן עושים מהלך חדש מנקודה זו, אנו זורקים את כל ההיסטוריה “העתידית” שעכשיו תיהפך לשגויה.

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

לסיום, נשנה את מתודת render של קומפוננטת המשחק מרינדור קבוע של המהלך האחרון לרינדור של המהלך שבחור כעת לפי stepNumber:

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    // the rest has not changed

אם נלחץ על כל צעד בהיסטורית המשחק, לוח האיקס-עיגול צריך להתעדכן באופן מיידי כדי להראות איך הלוח נראה לאחר שצעד זה התרחש.

צפו בקוד המלא עד נקודה זו

לסיום

ברכותינו! יצרתם משחק איקס-עיגול אשר:

  • מאפשר לכם לשחק איקס-עיגול,
  • מציין מתי ששחקן ניצח במשחק,
  • שומר הסטוריית משחק ככל שהמשחק מתקדם,
  • מאפשר לשחקנים לסקור את היסטוריית המשחק ולראות גרסאות קודמות של לוח המשחק.

עבודה טובה! אנו מקווים כי עכשיו אתם מרגישים שיש לכם הבנה טובה על איך עובדת React.

בדקו את התוצאה הסופית כאן: תוצאה סופית.

אם יש לכם זמן נוסף או שתרצו לתרגל את מיומנויות React החדשות שלכם, הנה כמה רעיונות לשיפורים שאתם יכולים לעשות למשחק האיקס-עיגול אשר מוצגים בסדר קושי עולה:

  1. הציגו את המיקום עבור כל מהלך בתבנית (עמודה, שורה) ברשימת ההיסטוריה של המהלכים.
  2. הדגישו את הפריט שבחור כעת ברשימת המהלכים.
  3. שכתבו את הלוח כך שישתמש בשתי לולאות כדי לייצר את הריבועים במקום שיהיו כתובים בקידוד קשיח (hardcoded).
  4. הוספת כפתור “החלפה” המאפשר למיין את המהלכים בסדר עולה או יורד.
  5. כאשר מישהו זוכה, הדגישו את שלושת הריבועים שגרמו לניצחון.
  6. כאשר אין זוכה, הציגו הודעה על כך שהתוצאה היא תיקו.

במהלך מדריך זה, נגענו בקונספטים של React כולל אלמנטים, קומפוננטות, props, ו-state. לקבלת הסבר מפורט יותר על כל אחד מהנושאים הללו, עיינו בשאר התיעוד. כדי ללמוד עוד אודות הגדרת קומפוננטות, עיינו ב-React.Component API reference.