בחירת מבנה המדינה

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

You will learn

  • מתי use משתנים בודדים לעומת state מרובים
  • ממה להימנע בעת ארגון state
  • כיצד לתקן בעיות נפוצות במבנה state

עקרונות לבניית state

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

  1. קשור לקבוצה state. אם אתה תמיד מעדכן שני משתני state או יותר בו-זמנית, שקול למזג אותם למשתנה state יחיד.
  2. הימנע מסתירות בstate. כאשר ה-state בנוי בצורה שכמה חלקים של state עלולים לסתור ו”לא להסכים” זה עם זה, אתם משאירים מקום לטעויות. נסו להימנע מכך.
  3. הימנע מיותר state. אם אתה יכול לחשב מידע מה-props של הרכיב או משתני state הקיימים שלו במהלך העיבוד, אל תכניס את המידע הזה ל-state של אותו רכיב.
  4. הימנע משכפול ב-state. כאשר אותם נתונים משוכפלים בין מספר משתני state, או בתוך אובייקטים מקוננים, קשה לשמור אותם מסונכרנים. צמצם כפילות כשאפשר.
  5. הימנע מ-state מקוננת עמוקה. state היררכי עמוק לא נוח לעדכון. במידת האפשר, העדיפו לבנות את state בצורה שטוחה.

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

עכשיו בואו נראה כיצד העקרונות הללו חלים בפעולה.

לפעמים אתה לא בטוח בין שימוש במשתני state בודדים או מרובים.

האם כדאי לעשות זאת?

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

או זה?

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

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

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>
  )
}

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

Pitfall

אם המשתנה state שלך הוא אובייקט, זכור שאינך יכול לעדכן רק שדה אחד בו מבלי להעתיק במפורש את השדות האחרים. לדוגמה, אתה לא יכול לעשות setPosition({ x: 100 }) בדוגמה שלמעלה כי use לא יהיה לו את המאפיין y בכלל! במקום זאת, אם תרצו להגדיר את x לבד, הייתם עושים setPosition({ ...position, x: 100 }), או מפצלים אותם לשני משתנים state ועושים setX(100).

הימנע מסתירות ב-state

להלן טופס משוב על מלון עם משתנים isSending וisSent state:

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

בעוד שקוד זה עובד, הוא משאיר את הדלת פתוחה ל-states “בלתי אפשריים”. לדוגמה, אם תשכחו להתקשר ל-setIsSent ו-setIsSending ביחד, אתם עלולים להגיע למצב שבו גם isSending וגם isSent הם true בו-זמנית. ככל שהרכיב שלך מורכב יותר, כך קשה יותר להבין מה קרה.

מכיוון שisSending וisSent לעולם לא יהיו true בו-זמנית, עדיף להחליף אותם במשתנה status state אחד שעשוי לקחת אחד מתוך שלושה states תקפים: 'typing' (ראשוני), 'sending', 'sending'

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

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

const isSending = status === 'sending';
const isSent = status === 'sent';

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

הימנע מיותר state

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

לדוגמה, קח את הטופס הזה. זה עובד, אבל האם אתה יכול למצוא בו state מיותר?

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

טופס זה כולל שלושה משתני state: firstName, lastName ו-fullName. עם זאת, fullName מיותר. אתה תמיד יכול לחשב fullName מfirstName וlastName במהלך העיבוד, אז הסר אותו מstate.

כך תוכל לעשות זאת:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

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

const fullName = firstName + ' ' + lastName;

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

Deep Dive

אל תשקף props ב-state

דוגמה נפוצה למיותר state היא קוד כזה:

function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);

כאן, משתנה color state מאותחל ל-messageColor מאפיין. הבעיה היא שאם רכיב האב יעביר ערך אחר של messageColor מאוחר יותר (לדוגמה, 'red' במקום 'blue'), המשתנה color state לא יתעדכן! ה-state מאותחל רק במהלך העיבוד הראשון.

זו הסיבה ש”שיקוף” של אביזר כלשהו במשתנה state יכול להוביל לבלבול. במקום זאת, use messageColor אבזר ישירות בקוד שלך. אם אתה רוצה לתת לו שם קצר יותר, use קבוע:

function Message({ messageColor }) {
const color = messageColor;

כך הוא לא ייצא מסנכרון עם האביזר שהועבר מהרכיב האב.

”שיקוף” props לתוך state הגיוני רק כאשר אתה רוצה להתעלם מכל העדכונים עבור אביזר ספציפי. לפי מוסכמה, התחל את שם האביזר ב-initial או default כדי להבהיר שמתעלמים מהערכים החדשים שלו:

function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const [color, setColor] = useState(initialColor);

הימנע שכפול ב-state

רכיב רשימת תפריט זה מאפשר לך לבחור חטיף נסיעה בודד מתוך כמה:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

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

למה זו בעיה? בואו נהפוך כל פריט לניתן לעריכה:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

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

למרות שתוכל לעדכן גם את selectedItem, תיקון קל יותר הוא להסיר כפילות. בדוגמה זו, במקום אובייקט selectedItem (שיוצר כפילות עם אובייקטים בתוך items), אתה מחזיק את ה-selectedId ב-state, ואז מקבל את ה-selectedItem על-ידי חיפוש במערך items אחר פריט עם המזהה הזה:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

יש לשכפל את ה-state used כך:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

אבל אחרי השינוי זה ככה:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

השכפול נעלם, ואתה שומר רק את ה-state החיוני!

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

הימנע מ-state מקוננות עמוקות

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

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'South Africa',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Americas',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brazil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canada',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'Mexico',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trinidad and Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Asia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'India',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europe',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'France',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Moon',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Mars',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

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

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

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

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botswana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egypt',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Morocco',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigeria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'South Africa',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Americas',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'India',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'South Korea',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thailand',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europe',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'France',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Germany',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turkey',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Australia',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'New Zealand',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Moon',
    childIds: [43, 44, 45]
  },
  43: {
    id: 43,
    title: 'Rheita',
    childIds: []
  },
  44: {
    id: 44,
    title: 'Piccolomini',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Tycho',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Mars',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

עכשיו, כשה-state הוא “שטוח” (ידוע גם כ”מנורמל”), עדכון הפריטים המקוננים נעשה קל יותר.

כדי להסיר מקום כעת, עליך לעדכן רק שתי רמות של state:

  • הגרסה המעודכנת של מקום הורה שלו צריכה לא לכלול את המזהה שהוסר מהמערך childIds שלו.
  • הגרסה המעודכנת של אובייקט הבסיס “טבלה” צריכה לכלול את הגרסה המעודכנת של מקום האב.

הנה דוגמה לאופן שבו אתה יכול לעשות את זה:

import { useState } from 'react';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);

  function handleComplete(parentId, childId) {
    const parent = plan[parentId];
    // Create a new version of the parent place
    // that doesn't include this child ID.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Update the root state object...
    setPlan({
      ...plan,
      // ...so that it has the updated parent.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => {
        onComplete(parentId, id);
      }}>
        Complete
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

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

Deep Dive

שיפור memoשימוש

באופן אידיאלי, תוכל גם להסיר את הפריטים שנמחקו (והילדים שלהם!) מהאובייקט “טבלה” כדי לשפר את השימוש memory. הגרסה הזו עושה את זה. זה גם uses 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": {}
}

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

Recap

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

Challenge 1 of 4:
תקן רכיב שאינו מעדכן

רכיב Clock זה מקבל שני props: color ו-time. כאשר אתה בוחר צבע אחר בתיבת הבחירה, הרכיב Clock מקבל אבזר color שונה מהרכיב האב שלו. עם זאת, מסיבה כלשהי, הצבע המוצג אינו מתעדכן. מַדוּעַ? תקן את הבעיה.

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}