ניהול State של פקד

1. הגדרת State

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

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

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

var Counter = React.createClass({
    getInitialState: function() {
        return { clicks: 0 };
    },
});

2. עדכון State באמצעות הפונקציה setState

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

אחד הדברים המפתיעים בקוד הוא הקריאה ל setState. זהו קוד הקריאה:

  onClick: function(e) {
    this.setState({ clicks: this.state.clicks + 1 });
  },

במבט ראשון התחביר נראה מוזר. למה לא פשוט להפעיל this.state.clicks++ ? התשובה קשורה בקשר אדוק לאופן בו ריאקט עובד ולאופן בו הפקדים מגיעים למסך.

3. מעקב אחר שינויים בריאקט

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

document.body.textContent = "Hello World";

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

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

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

4. אזהרה: הפונקציה setState עשויה להיות אסינכרונית

התבוננו בקוד הפקד הבא ונסו למצוא את הטעות:

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

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

  onClick: function(e) {
    this.setState({ clicks: this.state.clicks + 1 }, function() {
      this.setState({ clicks: this.state.clicks + 1 });
    }.bind(this));
  },

5. מפל המידע של React

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

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

6. סיכום

לצורך הגדרת State לפקד נוסיף את הפונקציה getInitialState. על כל שינוי ב State נדווח באמצעות הפעלת הפונקציה המיוחדת setState.

הפקד Clicker הבא מבצע את שתי הפעולות:

var Clicker = React.createClass({
  getInitialState() {
    return { clicks: 0 };
  },

  click() {
    this.setState({
      clicks: this.state.clicks + 1,
    });
  },

  render() {
    return (<div>
        <p>You clicked {this.state.clicks} times. 
          <button onClick={this.click}>Click Here</button>          
        </p>
      </div>);
  }
});

במקרים בהם יש יותר משדה אחד ב State, הפעלת הפונקציה setState משלבת (merge) את האוביקט שקיבלה עם המידע שכבר קיים ב State.

קוד הפקד שהוצג בפרק (לחצו להפעלה):


לצורך הגדרת State לפקד נוסיף את הפונקציה getInitialState. על כל שינוי ב State נדווח באמצעות הפעלת הפונקציה המיוחדת setState.

הפקד Clicker הבא מבצע את שתי הפעולות:

var Clicker = React.createClass({
  getInitialState() {
    return { clicks: 0 };
  },

  click() {
    this.setState({
      clicks: this.state.clicks + 1,
    });
  },

  render() {
    return (<div>
        <p>You clicked {this.state.clicks} times. 
          <button onClick={this.click}>Click Here</button>          
        </p>
      </div>);
  }
});

במקרים בהם יש יותר משדה אחד ב State, הפעלת הפונקציה setState משלבת (merge) את האוביקט שקיבלה עם המידע שכבר קיים ב State.

קוד הפקד שהוצג בפרק (לחצו להפעלה):