• בלוג
  • ריאקט, קלט מספרי ועיקרון ההפתעה הקטנה ביותר

ריאקט, קלט מספרי ועיקרון ההפתעה הקטנה ביותר

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

function App() {
  const [text, setText] = React.useState('');

  return (
    <div>
      <input type="number" value={text} onChange={e => setText(e.target.value)} />
      <p>Text: {text}</p>      
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('main'));

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

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

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

בשביל להבין מה קורה המשכנו לכתוב דוגמא קצת יותר מתוחכמת:


function App() {
  const [numericInputChanges, setNumericInputChanges] = React.useState(0);
  const [textInputChanges, setTextInputChanges] = React.useState(0);
  const [numericInput, setNumericInput] = React.useState('');
  const [textInput, setTextInput] = React.useState('');

  function reset() {
    setTextInput('');
    setNumericInput('');
    setTextInputChanges(0);
    setNumericInputChanges(0);    
  }

  function numericInputChanged(e) {
    setNumericInputChanges(v => v + 1);    
    setNumericInput(e.target.value);
  }

  function textInputChanged(e) {
    setTextInputChanges(v => v + 1);
    setTextInput(e.target.value);
  }

  return (
    <div>
      <p>Numeric Input Changes: {numericInputChanges}</p>
      <p>Text Input Changes: {textInputChanges}</p>      
      <input type="text" value={textInput} onChange={textInputChanged} />
      <input type="number" value={numericInput} onChange={numericInputChanged}  />
      <button type="reset" onClick={reset} >Reset</button>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('main'));

וכן, יש גם קודפן:

הפעם אני מציג גם Counter שעולה ב-1 כל פעם שקומפוננטת ה input מפעילה את הפונקציה שכתובה ב onChange. הנה מה שקורה שם:

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

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

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

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

עריכה - ותודה לגדי על התיקון

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