Menu

עדכון מערכים ואובייקטים ב-state של React (בלי mutation)

כדי לשנות מערך או אובייקט ב-state של React, בונים חדש ומעבירים אותו ל-setter. למדו איך מוסיפים, מסירים, מחליפים, מכניסים וממיינים פריטים, מעדכנים אובייקטים מקוננים, ולמה mutation של state משאיר את המסך בלי שינוי.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

ב-React מעדכנים מערך או אובייקט ב-state על ידי יצירה של חדש והעברתו ל-setter. אף פעם אל תשנו את הערך הקיים במקום: השתמשו ב-spread (...), map, filter או slice כדי לבנות עותק עם השינוי שלכם, ואז קראו ל-setItems(copy).

[...items, { ... }] הוא מערך חדש עם פריט אחד נוסף, ו-filter מחזיר מערך חדש בלי הפריט שהוסר. שניהם לא נוגעים במערך הישן.

למה mutation לא מרנדר

כשקוראים ל-setter, React משווה את הערך החדש לישן בעזרת Object.is. במערכים ובאובייקטים זה משווה הפניות, לא תוכן. אם משנים מערך עם push ומעבירים בחזרה את אותו מערך, React רואה את אותה הפניה, מחליטה ששום דבר לא השתנה, ומדלגת על הרינדור.

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

מערכים: מתודות לשימוש ומתודות להימנע מהן

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

מטרהלהימנע (mutation)להשתמש (מחזיר מערך חדש)
הוספהpush, unshift[...arr, item], [item, ...arr]
הסרהpop, shift, splicefilter, slice
החלפהarr[i] = x, splicemap, with(i, x)
הכנסהsplice[...arr.slice(0, i), x, ...arr.slice(i)], toSpliced
מיון, היפוךsort, reversetoSorted, toReversed, או להעתיק קודם

toSorted, toReversed, toSpliced ו-with הן מתודות מערך חדשות יותר (ES2023), שנתמכות בכל הדפדפנים המרכזיים העדכניים מאז 2023. הן עושות את אותה עבודה כמו המקבילות שלהן שמשנות את המערך, אבל על עותק.

עדכון פריט אחד עם map

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

החלק { ...todo, done: !todo.done } חשוב. באג נפוץ הוא להעתיק את המערך אבל לשנות את האובייקט שבתוכו:

const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);

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

הכנסה באינדקס

אין insert שלא עושה mutation, ולכן חותכים את המערך לשניים ושמים את הפריט החדש בין החצאים. toSpliced(index, 0, item) עושה את אותו דבר בקריאה אחת.

קבעו את האינדקס ל-0 והכניסו שוב: ה-C החדש הולך להתחלה. החליפו את הביטוי של next ב-letters.toSpliced(index, 0, 'C') והתוצאה זהה.

מיון והיפוך

sort ו-reverse משנים את המערך שעליו קוראים להם, למרות שהם גם מחזירים אותו. זה הופך את setItems(items.sort()) ל-mutation ואחריה set עם אותה הפניה, שזה הבאג מתחילת הדף. מיינו עותק במקום.

[...scores].reverse() עובד כי ה-spread יוצר את העותק קודם ו-reverse משנה אחר כך רק את העותק הזה.

עדכון אובייקטים ואובייקטים מקוננים

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

כל רמה שמעתיקים היא אובייקט חדש; כל רמה שלא נוגעים בה נשארת אותו אובייקט. זה מה שהופך את ה-spread לזול: אם ה-state היה מחזיק גם רשימה ארוכה של הזמנות, { ...user } היה מעתיק את ההפניה לרשימה, לא את הרשימה עצמה.

אם אובייקט ב-state מקונן שלוש או ארבע רמות לעומק ואתם כל הזמן כותבים שרשראות ארוכות של spreads, זה לעיתים קרובות סימן שה-state יכול להיות שטוח יותר. שמירת פריטים בטבלת חיפוש לפי id, או פיצול אובייקט גדול אחד לכמה קריאות useState, שומרים כל עדכון קצר. כשהרבה אירועים משנים מבנה אחד, גם reducer עוזר (ראו useReducer).

structuredClone מול spread

structuredClone(user) יוצר העתק עמוק בקריאה אחת, כך שאחר כך אפשר לשנות את העותק בחופשיות:

const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);

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

כתיבת mutations עם Immer

Immer היא ספרייה קטנה שמאפשרת לכתוב קוד שנראה כמו mutation. היא נותנת לכם טיוטה (draft), רושמת מה אתם משנים, ומייצרת ערך immutable חדש שבו רק החלקים שהשתנו הועתקו. החבילה use-immer עוטפת אותה בהוק:

import { useImmer } from 'use-immer';

function Profile() {
    const [user, updateUser] = useImmer({
        name: 'Ada',
        address: { city: 'London' },
    });

    function moveTo(city) {
        updateUser((draft) => {
            draft.address.city = city;
        });
    }
    // ...
}

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

שאלות נפוצות

איך מוסיפים פריט למערך ב-state של React?

יוצרים מערך חדש שמכיל את הפריטים הישנים ועוד את החדש ומעבירים אותו ל-setter: setItems([...items, newItem]). אל תקראו ל-items.push(newItem), כי זה משנה את המערך הקיים ו-React רואה את אותה הפניה.

למה הקומפוננטה שלי לא מתרנדרת מחדש כשאני מעדכן מערך?

כנראה שיניתם את המערך במקום (push, splice, sort או arr[i] = x) והעברתם בחזרה את אותו מערך. React משווה את הערך הישן לחדש עם Object.is, מוצאת את אותו אובייקט, ומדלגת על הרינדור. העבירו מערך חדש במקום.

איך מעדכנים אובייקט אחד בתוך מערך ב-React?

השתמשו ב-map והחזירו אובייקט חדש עבור הפריט שמשתנה, ואת הפריט המקורי עבור כל השאר: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).

איך מעדכנים אובייקט מקונן ב-state של React?

מעתיקים כל רמה במסלול אל השדה שמשנים: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). spread יחיד מעתיק רק את הרמה העליונה, כך שהאובייקט הפנימי עדיין היה משותף.

להשתמש ב-structuredClone או באופרטור spread בשביל state של React?

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

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל