הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

מה קרה כשניסיתי לתרגם קוד מ jQuery ל React

27/01/2020

קיטור קטן בשביל לפתוח את השבוע והפעם על הקושי להסביר את ריאקט דרך דוגמאות פשוטות. קחו את הקוד הבא לדוגמא ב jQuery:

// Write React front end to talk to this server
async function refresh() {
    const messages = await $.get('/api/v1.0/messages');
    $('ul').html('');
    $.each(messages, function() {
        $('ul').append(`<li>${this.from} - ${this.text}</li>`);
    });
}

$('form').on('submit', async function(e) {
    e.preventDefault();
    const from = $('input[name="from"]').val();
    const text = $('input[name="text"]').val();
    const msg = { from, text };

    await $.post('/api/v1.0/messages', msg);
    $('ul').append(`<li>${from} - ${text}</li>`);
});

$('#btn-refresh').on('click', refresh);

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

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

בתרגום לריאקט הרבה מהקסם הולך לאיבוד. הנה אותו מנגנון בגירסת React ו TypeScript:

import React, {FormEvent, useState} from "react";
import $ from "jquery";

interface IMessage {
    from: string,
    text: string,
}

type EmptyFunction = () => void;

function ListMessages(props: {refresh: EmptyFunction, messages: IMessage[] }) {
    const { refresh, messages } = props;
    return (
        <>
            <button onClick={refresh}>Refresh</button>
            <ul>
                {messages.map((msg) => (
                    <li>{msg.from} - {msg.text}</li>
                ))}
            </ul>
        </>
    )

}

function NewMessage(props: {send: (from: string, text: string) => void}) {
    const [from, setFrom] = useState('');
    const [text, setText] = useState('');
    const { send } = props;

    async function handleSubmit(e: FormEvent) {
        e.preventDefault();
        await send(from, text);
        setFrom('');
        setText('');
    }

    return (
      <form onSubmit={handleSubmit}>
        <label>
          From:
          <input
              type="text"
              name="from"
              value={from}
              onChange={(e) => setFrom(e.target.value)}
          />
        </label>

        <label>
          Text:
          <input
              type="text"
              name="text"
              value={text}
              onChange={(e) => setText(e.target.value)}
          />
        </label>

        <input type="submit" value="Send" />
      </form>
    )
}

export default function MessagesPage(props: {}) {
    const [messages, setMessages] = useState<IMessage []>([]);

    async function refresh() {
        const res = await $.get('http://localhost:3000/api/v1.0/messages');
        setMessages(res);
    }

    async function send(from: string, text: string) {
        const msg = { from, text };
        await $.post('http://localhost:3000/api/v1.0/messages', msg);
        setMessages([...messages, msg]);
    }

    return (
        <>
            <NewMessage send={send} />
            <ListMessages messages={messages} refresh={refresh} />
        </>
    )
}

במקום לקבל קוד JavaScript נקי יש לנו חיבור בין קוד התצוגה לקוד הלוגיקה. ה HTML הוא ממש בתוך הפרצוף. גם החלוקה בין הקומפוננטות נראית מלאכותית כי ListMessages ו NewMessage לא באמת יכולות לעבוד לבד ותמיד חייבות להופיע בתוך MessagesPage בשביל לקבל את ה props המתאימים.

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

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

פרויקטים שאי אפשר לטעות בהם

26/01/2020

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

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

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

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

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

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

היכולת לרצות

25/01/2020

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

אפילו שבמקומות אחרים ובחברות אחרות דברים עובדים אחרת לגמרי.

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

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

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

איך להפוך Class Component ל Function Component ב React

24/01/2020

למרות שאין בעיה להמשיך לעבוד עם Class Components, קורה לפעמים שנמצא דוגמא ברשת של Class Component ונעדיף להפוך אותה ל Function Component לפני שמשלבים אותה ביישום שלנו.

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

https://github.com/josdejong/jsoneditor/tree/develop/examples/react_demo

והקובץ המרכזי ממנה הוא הפקד הבא:

import React, {Component} from 'react';

import JSONEditor from 'jsoneditor';
import 'jsoneditor/dist/jsoneditor.css';

import './JSONEditorDemo.css';

export default class JSONEditorDemo extends Component {
  componentDidMount () {
    const options = {
      mode: 'tree',
      onChangeJSON: this.props.onChangeJSON
    };

    this.jsoneditor = new JSONEditor(this.container, options);
    this.jsoneditor.set(this.props.json);
  }

  componentWillUnmount () {
    if (this.jsoneditor) {
      this.jsoneditor.destroy();
    }
  }

  componentDidUpdate() {
    this.jsoneditor.update(this.props.json);
  }

  render() {
    return (
        <div className="jsoneditor-react-container" ref={elem => this.container = elem} />
    );
  }
}

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

המשך קריאה

עבודת צוות

23/01/2020

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

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

בישיבה הבאה שלכם, בהאקתון הבא שלכם, ב Team Programming או Code Review הבא שלכם או אפילו בשבוע עבודה הרגיל אצכלם בעבודה נסו לשים לב: מי האנשים שאי אפשר בלעדיהם? מה הם עשו מיוחד השבוע? ומה אני יכול לנסות לעשות שבוע הבא כדי להיכנס למועדון הזה?

משהו מקולקל בשאלות מראיונות עבודה

22/01/2020

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

דוגמא ראשונה היא קישור בשם 37 Essential JavaScript Interview Questions and Answers. השאלה הראשונה שם היא מה הבעיה עם הבדיקה:

typeof bar === "object"

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

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

מספר 3 ברשימה מכיל רשימה של 50 שאלות מראיונות (אתר בשם אדוריקה). הם פחות בקטע של להראות קטעי קוד ובמקום שואלים שאלות כמו "מה ההבדל בין call ל apply". עכשיו צריך להגיד זאת שאלה מצוינת ולדעתי כל מתכנת JavaScript צריך להכיר את התשובה, ועדיין אם אני צריך להחליט עם איזה מתכנתים לעבוד קשה להאמין שהייתי משתמש בשאלה כזו בתור איזשהו פרמטר. אותו דבר לגבי "איך לרוקן מערך ב JavaScript" או מה עושה Object.create.

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

במקום לשאול מה עושה Object.create אולי עדיף לשאול - הפקודה Object.create יוצרת אוביקט חדש שה Prototype שלו מוגדר להיות אוביקט אחר. באיזה מצבים נראה לך שהיינו משתמשים בה? יש לך רעיון איך לממש אותה מאפס?

או במקום לשאול מה ההבדל בין call ל apply הייתי שואל "הפקודה apply מקבלת פונקציה, משתנה this ומערך של פרמטרים ומפעילה את הפונקציה עם המשתנה this ומערך הפרמטרים שהעברנו. באיזה מצבים נראה לך שמשתמשים בפקודה כזו? איך היית בונה מנגנון כזה לבד?"

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

שמות

21/01/2020

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

השוו את זה לפונקציה toJS של Immutable.JS - הפונקציות fromJS ו toJS משמשות להמרת מבנה נתונים ממבנה של Immutable JS למבנה רגיל של JavaScript, המרה שמצריכה סריקת עומק של כל מבנה הנתונים והעתקת כל הנתונים. אלה פונקציות שאם תשתמשו בהן יהרגו את הביצועים של היישום שלכם, ובחיבור לריאקט עושות נזק כפול כי הן מקלקלות את ה Referential Equality וגורמות לזה שצריך לרנדר מחדש את כל הקומפוננטות שלנו (כי אנחנו לא יודעים מה באמת השתנה).

מה שיותר גרוע הוא של Immutable.JS יש למעשה שתי פונקציות המרה: האחת נקראת toJS והשניה toJSON, עם הבדל קריטי ביניהן. הפונקציה toJS מבצעת העתקה עמוקה ולעומתה toJSON מבצעת העתקה שטחית רק של הרמה הראשונה באוביקט.

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

אבל אני כבר יודע ריאקט

20/01/2020

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

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

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

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

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

נ.ב. במרץ אני מתכנן לפתוח מסלול לימוד כזה שישלב עבודה על פרויקט-צד, ליווי של מנטור ומיטאפים וירטואלים כדי ללמוד ולהשתפר בריאקט. המסלול מתאים למתכנתים שכבר יודעים ריאקט, או למתכנתי JavaScript שרוצים להיכנס לעניינים ויימשך חודשיים. לפרטים שווה להעיף מבט בדף הקורס בקישור: https://www.tocode.co.il/live_courses/1

האותיות הקטנות

19/01/2020

בקורס פייתון שלימדתי השבוע חשבתי שיהיה נחמד לשלב שימוש ב Google Translate באחד התרגולים. לפני הקורס חיפשתי בגוגל חבילה לגשת מ Python ל Google Translate וכמובן תוצאה ראשונה היתה חבילה בשם googletrans.

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

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

ue to limitations of the web version of google translate, this API does not guarantee that the library would work properly at all times (so please use this library if you don’t care about stability).

זה קטן, זה בסוגריים, וזה מסביר הכל. החבילה היא לא דרך רשמית לגשת ל Google Translate. כותב החבילה ניגש לממשק ה Web של השירות, וכשניסינו להריץ את השאילתות מהכיתה הממשק זיהה אותנו בתור מתקפת DOS וחסם את הנתב הכיתתי.

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

בואו נכתוב משחק זיכרון ב React עם useReducer

18/01/2020

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

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

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

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

const initialState = {
  visibleCards: new Set(),
  currentTurn: new Set()
};

כל לחיצה על קלף מוסיפה אותו ל currentTurn, וכשזה מתמלא אנחנו (אולי) מעבירים את הקלפים משם ל visibleCards ובכל מקרה מאפסים את ה currentTurn. את כל הלוגיקה הזאת אני מעדיף לא לכתוב בתוך קוד הקומפוננטה: אני אוהב שהקומפוננטות שלי כוללות כמה שפחות קוד ומתמקדות רק באיך להציג את המידע על המסך. האלטרנטיבה, כאשר יש לי לוגיקת סטייט מורכבת שקשורה לקומפוננטה בודדת היא הפונקציה useReducer.

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

const reducer = produce((state, { type, payload }) => {
  const { cards } = payload;
  if (state.currentTurn.size === 2) {
    startNewTurn(state, cards);
  }

  if (type === "click") {
    const { idx } = payload;
    if (!state.currentTurn.has(idx) && !state.visibleCards.has(idx)) {
      state.currentTurn.add(payload.idx);
    }
  }
});

function startNewTurn(state, cards) {
  const turnCards = Array.from(state.currentTurn);
  if (turnCards.every(c => cards[c] === cards[turnCards[0]])) {
    state.visibleCards = new Set([...state.visibleCards, ...state.currentTurn]);
  }
  state.currentTurn = new Set();
}

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

function MemoryGame(props) {
  const { cards } = props;
  const [state, dispatch] = useReducer(reducer, initialState);

  function cardClasses(state, idx) {
    let res = "card";
    if (state.visibleCards.has(idx) || state.currentTurn.has(idx)) {
      res += " visible";
    }
    return res;
  }

  return (
    <div className="game">
      <ul>
        {cards.map((val, idx) => (
          <li
            className={cardClasses(state, idx)}
            onClick={() => dispatch({ type: "click", payload: { idx, cards } })}
          >
            {val}
          </li>
        ))}
      </ul>
    </div>
  );
}

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

https://codesandbox.io/s/billowing-river-tbmzu