useState הוא ההוק שמאפשר לקומפוננטה לזכור משהו בין רינדורים: ספירה, הטקסט בשדה קלט, האם תפריט פתוח. בכל פעם שקוראים ל-setter שלו, React מרנדרת את הקומפוננטה שוב עם הערך החדש.
לחצו על הכפתור, ואז שנו את useState(0) ל-useState(10) בעורך. התצוגה המקדימה נבנית מחדש והספירה מתחילה מ-10.
התחביר
const [value, setValue] = useState(initialValue);
initialValueמשמש רק ברינדור הראשון. בכל רינדור מאוחר יותר React מתעלמת ממנו ומחזירה את הערך שהיא שמרה.valueהוא הערך הנוכחי ברינדור הזה.setValueמחליף את הערך ומתזמן רינדור חדש.
השמות שלכם. useState מחזיר מערך של שני פריטים והסוגריים המרובעים מפרקים אותו, כך ש-[isOpen, setIsOpen] ו-[email, setEmail] הם אותו דפוס.
קראו ל-useState ברמה העליונה של הקומפוננטה, אף פעם לא בתוך לולאה, תנאי או פונקציה מקוננת. React מתאימה כל קריאה לערך השמור שלה לפי סדר הקריאות, ולכן הסדר הזה חייב להיות זהה בכל רינדור (ראו כללי ההוקים).
כמה חלקי state
קומפוננטה יכולה לקרוא ל-useState כמה פעמים שהיא צריכה. כל קריאה היא ערך עצמאי משלה.
state הוא תמונת מצב (snapshot)
ה-setter לא משנה את המשתנה שבידיכם. הוא אומר ל-React מה הערך צריך להיות ברינדור הבא. עד שהרינדור הזה קורה, הרינדור הנוכחי ממשיך לראות את הערך הישן.
פתחו את הקונסול מתחת לתצוגה המקדימה ולחצו פעם אחת. ה-handler רושם 0, הערך הישן, ואז הקומפוננטה רושמת רינדור חדש עם 1. שום דבר לא שגוי: count הוא קבוע לאורך כל הרינדור, והרינדור הבא מקבל אחד חדש.
פונקציות updater
בגלל תמונת המצב, קריאה ל-setter שלוש פעמים עם count + 1 מוסיפה אחד, לא שלושה: כל קריאה מחשבת 0 + 1. העבירו פונקציה במקום, ו-React מזינה לכל קריאה את התוצאה של הקודמת.
השתמשו בצורת ה-updater בכל פעם שהערך הבא תלוי בקודם והעדכון אולי לא רץ ישירות מהרינדור העדכני: כמה קביעות ב-handler אחד, callback של setInterval, או אפקט.
אובייקטים ומערכים ב-state
React מחליטה אם לרנדר שוב על ידי השוואה בין הערך הישן לחדש עם Object.is. אם משנים אובייקט במקום ומעבירים בחזרה את אותו אובייקט, ההשוואה אומרת ששום דבר לא השתנה והמסך נשאר כמו שהיה. תמיד העבירו אובייקט או מערך חדש.
{ ...user, age: user.age + 1 } מעתיק כל שדה ודורס אחד. [...tags, 'code'] מוסיף פריט, ו-filter מסיר אחד, ושניהם מחזירים מערך חדש. הדף על עדכון מערכים ואובייקטים מכסה אובייקטים מקוננים, עריכת פריט אחד ברשימה והכנסה באמצע.
state התחלתי עצל (lazy)
הארגומנט של useState מחושב בכל רינדור, למרות ש-React משתמשת בו רק בפעם הראשונה. כשהחישוב שלו יקר (קריאה מ-localStorage, פענוח של מחרוזת גדולה), העבירו פונקציה. React קוראת לה פעם אחת, ברינדור הראשון.
// Runs readSavedNotes() on every render, then throws the result away
const [notes, setNotes] = useState(readSavedNotes());
// Runs it once
const [notes, setNotes] = useState(readSavedNotes);
const [notes, setNotes] = useState(() => readSavedNotes());
איפוס state עם key
state שייך למיקום של קומפוננטה בעץ. אם מרנדרים key אחר באותו מקום, React מתייחסת לזה כקומפוננטה חדשה וה-state שלה מתחיל מחדש. זו הדרך הנקייה ביותר לאפס טופס כשהדבר שהוא עורך משתנה.
הקלידו הודעה, ואז החליפו נמען: הטיוטה מתנקה. מחקו את key={to} ונסו שוב, והטקסט נשאר, כי React רואה עכשיו את אותו Draft באותו מקום.
טעויות נפוצות
הדפסת state מיד אחרי שקבעתם אותו. רואים את הערך הישן, כמו שמוצג למעלה. הדפיסו בגוף הקומפוננטה, או חשבו קודם את הערך הבא לתוך משתנה והשתמשו בו.
שינוי ישיר (mutation) של state. user.age++; setUser(user) מעביר את אותו אובייקט, ולכן שום דבר לא מתרנדר. העתיקו קודם.
קריאה ל-setter בזמן הרינדור. <button onClick={setCount(count + 1)}> קורא ל-setCount בזמן הרינדור, מה שמרנדר שוב, מה שקורא לו שוב. React עוצרת את הלולאה הזו עם "Too many re-renders". העבירו פונקציה: onClick={() => setCount(count + 1)}.
שמירת ערכים שאפשר לחשב. אם fullName הוא תמיד first + ' ' + last, חשבו אותו בזמן הרינדור במקום להחזיק חלק state שלישי שעלול לצאת מסנכרון.
שאלות נפוצות
מה useState מחזיר?
מערך עם בדיוק שני פריטים: הערך הנוכחי ופונקציה שמחליפה אותו. אתם נותנים להם שמות בעצמכם עם destructuring של מערך, לפי המוסכמה [thing, setThing].
למה ה-state שלי לא מתעדכן מיד?
קריאה ל-setter לא משנה את המשתנה שכבר יש בידיכם. היא מבקשת מ-React לרנדר את הקומפוננטה שוב, והערך החדש מופיע ברינדור הבא. בתוך ה-event handler הנוכחי הערך הישן עדיין ב-scope, ולכן console.log(count) מיד אחרי setCount(count + 1) מדפיס את המספר הישן.
מתי כדאי להעביר פונקציה ל-setter?
כשהערך החדש תלוי בישן וקובעים אותו יותר מפעם אחת, או מתוך טיימר או אפקט. setCount(c => c + 1) תמיד מתחיל מהערך העדכני ביותר, ואילו setCount(count + 1) מתחיל מהערך שהרינדור הזה ראה.
האם useState יכול להחזיק אובייקט או מערך?
כן, אבל התייחסו אליו כאל קריאה בלבד. בנו אובייקט או מערך חדש והעבירו אותו ל-setter (setUser({ ...user, name }), setItems([...items, item])). שינוי של הקיים במקום לא מפעיל רינדור.
מה ההבדל בין useState ל-useReducer?
שניהם שומרים state. useState מתאים לכמה ערכים עצמאיים. useReducer מעביר את לוגיקת העדכון לפונקציה אחת שמקבלת action, מה שנקרא טוב יותר כשהרבה אירועים משנים את אותו state בדרכים קשורות.