עדכון אובייקטים ב-State

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

You will learn

  • כיצד לעדכן נכון אובייקט ב-React state
  • כיצד לעדכן אובייקט מקונן מבלי לשנות אותו
  • מהי אי-שינוי, ואיך לא לשבור אותה
  • איך להפוך את העתקת האובייקטים לפחות חוזרת על עצמה עם Immer

מהי מוטציה?

אתה יכול לאחסן כל סוג של ערך JavaScript ב-state.

const [x, setX] = useState(0);

עד כה עבדת עם מספרים, מחרוזות ובוליאנים. סוגים אלה של ערכי JavaScript הם “בלתי ניתנים לשינוי”, כלומר בלתי ניתנים לשינוי או “לקריאה בלבד”. אתה יכול להפעיל עיבוד מחדש ל-replace ערך:

setX(5);

ה-x state השתנה מ-0 ל-5, אך ה-מספר 0 עצמו לא השתנה. לא ניתן לבצע שינויים כלשהם בערכים הפרימיטיביים המובנים כמו מספרים, מחרוזות ובוליאנים ב-JavaScript.

עכשיו שקול אובייקט ב-state:

const [position, setPosition] = useState({ x: 0, y: 0 });

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

position.x = 5;

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

התייחס ל-state כאל קריאה בלבד

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

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

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        position.x = e.clientX;
        position.y = e.clientY;
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

הבעיה היא עם קטע הקוד הזה.

onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}

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

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

onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}

עם setPosition, אתה אומר לReact:

  • החלף את position באובייקט חדש זה
  • ועבד את הרכיב הזה שוב

שים לב כיצד הנקודה האדומה עוקבת כעת אחר המצביע שלך כאשר אתה נוגע או מרחף מעל אזור התצוגה המקדימה:

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

Deep Dive

מוטציה מקומית בסדר

קוד כזה הוא בעיה כיuse הוא משנה אובייקט קיים בstate:

position.x = e.clientX;
position.y = e.clientY;

אבל קוד כזה הוא בהחלט בסדר כיuse אתה עושה מוטציה לאובייקט חדש שיצרת זה עתה:

const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);

למעשה, זה שווה לחלוטין לכתוב את זה:

setPosition({
x: e.clientX,
y: e.clientY
});

מוטציה היא בעיה רק ​​כאשר אתה משנה אובייקטים קיימים שכבר נמצאים ב-state. שינוי של אובייקט שזה עתה יצרת זה בסדר מכיוון שעדיין אין קוד אחר שמפנה אליו.* שינוי זה לא ישפיע בטעות על משהו שתלוי בו. זה נקרא “מוטציה מקומית”. אתה יכול אפילו לעשות מוטציה מקומית תוך כדי רינדור. מאוד נוח ובסדר לגמרי!

העתקת אובייקטים עם תחביר הפיזור

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

שדות קלט אלה אינם פועלים מכיוון שuse המטפלים onChange משנים את ה-state:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    person.firstName = e.target.value;
  }

  function handleLastNameChange(e) {
    person.lastName = e.target.value;
  }

  function handleEmailChange(e) {
    person.email = e.target.value;
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

לדוגמה, שורה זו משנה את ה-state מעיבוד עבר:

person.firstName = e.target.value;

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

setPerson({
firstName: e.target.value, // New first name from the input
lastName: person.lastName,
email: person.email
});

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

setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});

עכשיו הטופס עובד!

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

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

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

Deep Dive

שימוש במטפל אירוע יחיד עבור שדות מרובים

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

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleChange(e) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label>
      <label>
        Last name:
        <input
          name="lastName"
          value={person.lastName}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input
          name="email"
          value={person.email}
          onChange={handleChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

כאן, e.target.name מתייחס למאפיין name שניתן לרכיב <input> DOM.

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

שקול מבנה אובייקט מקונן כמו זה:

const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});

אם רצית לעדכן את person.artwork.city, ברור איך לעשות את זה עם מוטציה:

person.artwork.city = 'New Delhi';

אבל ב-React, אתה מתייחס ל-state כבלתי ניתן לשינוי! כדי לשנות את city, תחילה יהיה עליך לייצר את האובייקט artwork החדש (מאוכלס מראש בנתונים מהקודם), ולאחר מכן לייצר את האובייקט person החדש שמצביע על artwork החדש:

const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

או, כתוב כקריאת פונקציה אחת:

setPerson({
...person, // Copy other fields
artwork: { // but replace the artwork
...person.artwork, // with the same one
city: 'New Delhi' // but in New Delhi!
}
});

זה נהיה קצת מלל, אבל זה עובד מצוין עבור מקרים רבים:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}

Deep Dive

אובייקטים לא ממש מקוננים

אובייקט כמו זה מופיע “מקנן” בקוד:

let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};

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

let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};

let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};

האובייקט obj1 אינו “בתוך” obj2. לדוגמה, obj3 יכול “להצביע” גם על obj1:

let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};

let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};

let obj3 = {
name: 'Copycat',
artwork: obj1
};

אם היית משנה את obj3.artwork.city, זה ישפיע גם על obj2.artwork.city וגם על obj1.city. זה בגלל שuse obj3.artwork, obj2.artwork וobj1 הם אותו אובייקט. קשה לראות את זה כשחושבים על חפצים כ”מקוננים”. במקום זאת, הם אובייקטים נפרדים “המצביעים” זה על זה עם מאפיינים.

כתוב היגיון עדכון תמציתי עם Immer

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

updatePerson(draft => {
draft.artwork.city = 'Lagos';
});

אבל בניגוד למוטציה רגילה, היא לא מחליפה את העבר state!

Deep Dive

איך Immer עובד?

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

כדי לנסות את Immer:

  1. הפעל את npm install use-immer כדי להוסיף Immer כתלות
  2. לאחר מכן החלף את import { useState } from 'react' ב-import { useImmer } from 'use-immer'

להלן הדוגמה שלמעלה המרה ל-Immer:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

שימו לב לכמה יותר תמציתיים הפכו מטפלי האירועים. אתה יכול לערבב ולהתאים את useState וuseImmer ברכיב בודד כמה שתרצה. Immer היא דרך מצוינת לשמור על מטפלי העדכונים תמציתיים, במיוחד אם יש קינון ב-state שלך, והעתקת אובייקטים מובילה לקוד שחוזר על עצמו.

Deep Dive

יש כמה סיבות:

  • ניפוי באגים: אם תעשה use console.log ולא תשנה את state, יומני העבר שלך לא יקבלו את השינויים האחרונים ב-state. אז אתה יכול לראות בבירור כיצד state השתנה בין עיבודים.
  • אופטימיזציות: React נפוצות אסטרטגיות אופטימיזציה מסתמכות על דילוג על עבודה אם props או state קודמים זהים לאלה הבאים. אם אף פעם לא תבצע מוטציה ב-state, זה מהר מאוד לבדוק אם היו שינויים כלשהם. אם prevObj === obj, אתה יכול להיות בטוח ששום דבר לא יכול היה להשתנות בתוכו.
  • תכונות חדשות: התכונות החדשות של React שאנו בונים מסתמכות על כך ש-state יטופלו כמו תמונת מצב.](/learn/state-as-a-snapshot) אם אתה משנה גרסאות קודמות של state, זה עלול למנוע ממך להשתמש בתכונות החדשות.
  • שינויי דרישה: תכונות מסוימות של יישום, כמו הטמעת ביטול/בצע מחדש, הצגת היסטוריית שינויים או מתן אפשרות ל-user לאפס טופס לערכים קודמים יותר, קלות יותר לביצוע כאשר שום דבר לא משתנה. זה בגלל use אתה יכול לשמור עותקים קודמים של state ב-memory, ולחדשuse אותם כאשר מתאים. אם אתה מתחיל עם גישה מוטטיבית, תכונות כמו זו יכול להיות קשה להוסיף מאוחר יותר.
  • יישום פשוט יותר: Because React אינו מסתמך על מוטציה, הוא לא צריך לעשות שום דבר מיוחד עם החפצים שלך. זה לא צריך לחטוף את המאפיינים שלהם, תמיד לעטוף אותם לתוך Proxies, או לעשות עבודה אחרת באתחול כפי שעושים הרבה פתרונות “ריאקטיביים”. זו גם הסיבה ש-React מאפשר לך להכניס כל אובייקט לתוך state—לא משנה כמה גדול—ללא מלכודות ביצועים נוספות או נכונות.

בפועל, לעתים קרובות אתה יכול “לברוח” עם מוטציה של state ב-React, אך אנו ממליצים בחום לא לעשות זאת כדי שתוכלו use תכונות חדשות של React שפותחו תוך מחשבה על גישה זו. תורמים עתידיים ואולי אפילו העצמי העתידי שלך יודו לך!

Recap

  • התייחס לכל state ב-React כבלתי ניתנים לשינוי.
  • כאשר אתה מאחסן אובייקטים ב-state, מוטציה שלהם לא תפעיל עיבודים ותשנה את ה-state ב-”תצלומי מצב” קודמים.
  • במקום לבצע מוטציה של אובייקט, צור גרסה חדשה שלו, והפעל עיבוד מחדש על ידי הגדרת state אליו.
  • אתה יכול use את תחביר הפצת האובייקט {...obj, something: 'newValue'} כדי ליצור עותקים של אובייקטים.
  • תחביר התפשטות רדוד: הוא מעתיק רק רמה אחת לעומק.
  • כדי לעדכן אובייקט מקונן, עליך ליצור עותקים עד למעלה מהמקום שאתה מעדכן.
  • כדי להפחית קוד העתקה חוזרת, use Immer.

Challenge 1 of 3:
תקן עדכוני state שגויים

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

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

import { useState } from 'react';

export default function Scoreboard() {
  const [player, setPlayer] = useState({
    firstName: 'Ranjani',
    lastName: 'Shettar',
    score: 10,
  });

  function handlePlusClick() {
    player.score++;
  }

  function handleFirstNameChange(e) {
    setPlayer({
      ...player,
      firstName: e.target.value,
    });
  }

  function handleLastNameChange(e) {
    setPlayer({
      lastName: e.target.value
    });
  }

  return (
    <>
      <label>
        Score: <b>{player.score}</b>
        {' '}
        <button onClick={handlePlusClick}>
          +1
        </button>
      </label>
      <label>
        First name:
        <input
          value={player.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={player.lastName}
          onChange={handleLastNameChange}
        />
      </label>
    </>
  );
}