לדלג לתוכן

משחק הזיכרון שילמד אתכם ריאקט טוב יותר מכל טוטוריאל

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

חוקי המשחק, בקצרה

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

הכנת חפיסת הקלפים

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

const baseIcons = ["A", "B", "C", "D", "E", "F"];

function createShuffledDeck() {
  const deck = [...baseIcons, ...baseIcons].map((icon, index) => ({
    id: index,
    icon,
    isFlipped: false,
    isMatched: false,
  }));
  return deck.sort(() => Math.random() - 0.5);
}

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

ניהול הקלפים ההפוכים

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

const [flippedCards, setFlippedCards] = useState([]);

function handleCardClick(id) {
  if (flippedCards.length === 2) return;
  setFlippedCards([...flippedCards, id]);
}

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

לוגיקת ההתאמה עם useEffect

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

useEffect(() => {
  if (flippedCards.length !== 2) return;

  const [firstId, secondId] = flippedCards;
  const first = deck.find((c) => c.id === firstId);
  const second = deck.find((c) => c.id === secondId);

  if (first.icon === second.icon) {
    setDeck((prev) =>
      prev.map((c) => (c.id === firstId || c.id === secondId ? { ...c, isMatched: true } : c))
    );
    setFlippedCards([]);
  } else {
    setTimeout(() => setFlippedCards([]), 1000);
  }
}, [flippedCards]);

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

זיהוי ניצחון

ניצחון הוא פשוט מקרה פרטי של "כל הקלפים תואמים" - אין צורך במשתנה state נפרד בשביל זה, אפשר לחשב אותו ישירות מתוך deck בכל רינדור:

const hasWon = deck.every((card) => card.isMatched);

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

המכשול הכי נפוץ - עדכון מערכים בצורה לא נכונה

טעות שכמעט כל מי שבונה את הפרויקט הזה עושה בפעם הראשונה - לנסות לעדכן קלף בודד במערך עם משהו כמו deck[index].isMatched = true. זה משנה את האובייקט "במקום" (mutation), ולריאקט אין דרך לדעת שמשהו השתנה, אז המסך לא מתעדכן. הפתרון תמיד זהה - ליצור מערך חדש עם map, כמו בדוגמה למעלה, ולהחזיר אובייקט חדש עבור הקלף שמשתנה.

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

הרחבות כיפיות

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

סיכום

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

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

ואם נתקעתם באמצע הלוגיקה של ההתאמה - יש קהילה שכבר פתרה את זה לא פעם.

הצטרפו לקהילה בדיסקורד