State स्ट्रक्चर चुनना

अछि तरह से State को स्ट्रक्चर करने से एक कौम्पोनॅन्ट के बीच अंतर पता लग सकता है जो चंगेस और डिबग करने में आराम दायक हो, और एक जो बग का एक निरंतर स्त्रोत ह। यहाँ दिए गए टिप्स state स्ट्रक्चर करते समय ध्यान में रखें।

You will learn

  • सिंगल और मल्टीपल state वेरिएबल्स का उपयोग कब करें
  • State को ऑर्गनाइज़ करते समय किन बातों से बचना चाहिए
  • State स्ट्रक्चर के सामान्य इश्यूज को कैसे ठीक करें

State स्ट्रक्टरिंग के प्रिंसिपल्स

जब आप एक कौम्पोनॅन्ट लिखते हैं जिसमें state का इस्तेमाल होता है, तो आपको इसके बारे में सोचना होता है की कितने state वेरिएबल्स का उपयोग करना है और उनके डेटा का शेप क्या होना चाहिए। हालांकि एक सबऑप्टिमल state स्ट्रक्चर के साथ भी सही प्रोग्राम लिखना पॉसिबल है, कुछ प्रिंसिपल्स हैं जो आपको बेहतर चोइसस बनाने के लिए गाइड कर सकते हैं:

  1. ग्रुप से संबंधित state। यदि आप हमेशा एक ही समय में दो या दो से ज्यादा state वेरिएबल्स को अपडेट करते हैं, तो उन्हें एक state वेरिएबल में मर्ज करने के बारे में सोचें।
  2. State में कन्ट्राडिक्शन्स से बचें। जब state को इस तरह से स्ट्रक्चर किया जाता है कि state के कई टुकड़े एक-दूसरे के साथ कन्ट्राडिक्ट और “असहमत” हो सकते हैं, तो आप गलतियों के लिए जगह छोड़ते हैं। इससे बचने की कोशिश करें।
  3. रिडेन्डेन्ट state से बचें। यदि आप रेंडरिंग के दौरान कौम्पोनॅन्ट के props या उसके मौजूदा state वेरिएबल्स से कुछ जानकारी कैलकुलेट कर सकते हैं, तो आपको उस जानकारी को उस कौम्पोनॅन्ट के state में नहीं रखना चाहिए।
  4. State में डुप्लिकेशन से बचें। जब एक ही डेटा को कई state वेरिएबल्स के बीच, या नेस्टेड ऑब्जेक्ट्स के बीच डुप्लिकेट किया जाता है, तो उन्हें सिंक में रखना मुश्किल होता है। जब आप कर सकते हैं तो डुप्लिकेशन कम करें।
  5. डीप नेस्टेड state से बचें। डीप्ली हायरार्किकल state को अपडेट करना आसान नहीं है। जब मुमकिन हो, state को फ्लैट तरीके से स्ट्रक्चर करें।

इन प्रिंसिपल्स के पीछे उद्देश्य है गलतियों को पेश किए बिना state को अपडेट करना आसान बनाएं। State से रिडेन्डेन्ट और डुप्लिकेट डेटा को हटाने से यह सुनिश्चित करने में मदद मिलती है कि इसके सभी टुकड़े सिंक में रहें। यह उसी तरह है जैसे एक डेटाबेस इंजीनियर बग की संभावना को कम करने के लिए डेटाबेस स्ट्रक्चर को “normalize” करना चाहता है। अल्बर्ट आइंस्टीन की व्याख्या करने के लिए, “अपने state को जितना हो सके उतना सरल बनाएं - लेकिन सरलतम नहीं।”

अब देखते हैं कि ये प्रिंसिपल्स कैसे लागू होते हैं।

आप कभी कभी एक या मल्टीपल state वेरिएबल्स का उपयोग करने के बीच अनिश्चित हो सकते हैं।

क्या आपको ऐसा करना चाहिए?

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

या यह?

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

तकनीकी तौर पर, आप इनमें से किसी भी तरीके का इस्तेमाल कर सकते हैं। लेकिन अगर कुछ दो 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>
  )
}

एक और मामला जहां आप डेटा को किसी ऑब्जेक्ट या array में समूहित करेंगे, जब आप नहीं जानते कि आपको state के कितने अलग-अलग टुकड़ों की ज़रूरत होगी। उदाहरण के लिए, यह तब मददगार होता है जब आपके पास एक फॉर्म होता है जहां उपयोगकर्ता कस्टम फ़ील्ड ऐड कर सकता है।

Pitfall

यदि आपका स्टेट वेरिएबल एक ऑब्जेक्ट है, तो याद रखें कि आप इसमें केवल एक फ़ील्ड को अपडेट नहीं कर सकते हैं अन्य फ़ील्ड्स को स्पष्ट रूप से कॉपी किए बिना। उदाहरण के लिए, आप setPosition({ x: 100}) नहीं कर सकते हैं ऊपर के उदाहरण में क्योंकि इसमें y प्रॉपर्टी बिल्कुल नहीं होगी! इसके बजाय, यदि आप अकेले x सेट करना चाहते हैं, तो आप या तो setPosition({ ...position, x: 100}) करेंगे, या उन्हें दो स्टेट वेरिएबल्स में विभाजित करेंगे और setX(100) करेंगे।

State में कंट्राडिक्शन्स से बचें

यहां isSending और isSent स्टेट वेरिएबल्स के साथ एक होटल फीडबैक फॉर्म दिया गया है:

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', और 'sent':

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, and fullName। हालांकि, fullName रिडेन्डेन्ट है। आप रेंडर के दौरान हमेशा firstName औरlastName से fullName को कैलकुलेट कर सकते हैं, इसलिए इसे 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

Don’t mirror props in state

रिडेन्डेन्ट state का एक सामान्य उदाहरण इस तरह का कोड है:

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

यहाँ, एक color state वेरिएबल को messageColor prop में इनिशियलाइज़ किया गया है। समस्या यह है कि यदि पैरेंट कौम्पोनॅन्ट बाद में messageColor का एक अलग वैल्यू पास करता है (उदाहरण के लिए, 'blue' के बजाय 'red'), तो color state वेरिएबल अपडेट नहीं होगा! State केवल पहले रेंडर के दौरान इनिशलाइज्ड होता है।

यही कारण है कि state वेरिएबल में कुछ prop को “मिरर” करने से कन्फ्यूजन पैदा हो सकता है। इसके बजाय, सीधे अपने कोड में messageColor props का उपयोग करें। यदि आप इसे छोटा नाम देना चाहते हैं, तो कॉन्स्टेंट का उपयोग करें:

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

इस तरह यह पैरेंट कौम्पोनॅन्ट से पास किए गए prop के साथ सिंक से बाहर नहीं होगा।

State में prop को “मिरर” करना तभी समझ में आता है जब आप किसी विशिष्ट prop के लिए सभी अपडेट को अनदेखा करना चाहते हैं। कन्वेंशन के अनुसार, prop के नाम को 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 में से एक आइटम के समान ऑब्जेक्ट है। इसका मतलब है कि आइटम के बारे में जानकारी दो जगहों पर डुप्लिकेट है।

यह समस्या क्यों है? आइए हर एक item को एडिटेबल बनाएं:

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

ध्यान दें की कैसे जब आप पहले आइटम के “Choose” पर क्लिक करते हैं और फिर उसे एडिट करते हैं, इनपुट अपडेट होता है लेकिन नीचे का लेबल एडिट को रिफ्लेक्ट नहीं करता है। ऐसा इसलिए है क्योंकि आपके पास डुप्लिकेट state है, और आप selectedItem को अपडेट करना भूल गए हैं।

हालाँकि आप selectedItem को भी अपडेट कर सकते हैं, लेकिन डुप्लीकेशन को निकालना आसान हल होगा। इस उदाहरण में, एक selectedItem ऑब्जेक्ट के बजाय (जो items के अंदर ऑब्जेक्ट्स के साथ डुप्लिकेशंस बनाता है), आप state में selectedId रख सकते हैं, और फिर उस ID के साथ item को items array में खोजकर selectedItem प्राप्त कर सकते हैं:

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

(इसके बजाये, आप selected index को state में रख सकते हैं।)

State को इस तरह डुप्लिकेट किया जाता था:

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

लेकिन बदलाव के बाद यह इस तरह है:

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

डुप्लिकेशन चला गया है, और आप केवल ज़रूरी state रखते हैं!

अब अगर आप selected item को एडिट करते हैं, तो नीचे दिया गया मैसेज तुरंत अपडेट हो जाएगा। ऐसा इसलिए है क्योंकि setItems फिर से रेंडर ट्रिगर करता है, और items.find(...) आइटम को अपडेट किये गए title के साथ ढूंढेगा। आपको selected item को state में रखने की ज़रूरत नहीं थी, क्योंकि केवल selected ID ही ज़रूरी है। बाकी की कैलकुलेशन रेंडर के दौरान की जा सकती है।

डीप नेस्टेड state से बचें

एक यात्रा प्लान की कल्पना करें जिसमें ग्रह, महाद्वीप और देश शामिल हों। आप नेस्टेड ऑब्जेक्ट्स और arrays का उपयोग करके इसकी 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 में child places की एक array होता है, आपके पास प्रत्येक स्थान में child place IDs की एक array को रख सकते हैं। फिर आप प्रत्येक place ID से संबंधित स्थान पर मैपिंग स्टोर कर सकते हैं।

यह डेटा रिस्ट्रक्चरिंग आपको डेटाबेस टेबल देखने की याद दिला सकता है:

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 “फ्लैट” (“normalized” के रूप में भी जाना जाता है) है, तो नेस्टेड आइटम को अपडेट करना आसान हो जाता है।

किसी स्थान को अभी निकालने के लिए, आपको केवल state के दो लेवल को अपडेट करने की ज़रूरत है:

  • अपडेटेड वर्शन के parent की जगह में हटाई गयी ID childIds array से एक्सक्लूड होनी चाहिए।
  • रूट “टेबल” ऑब्जेक्ट के अपडेटेड वर्जन में पैरेंट प्लेस का अपडेटेड वर्जन शामिल होना चाहिए।

आप इसके बारे में कैसे जा सकते हैं इसका एक उदाहरण यहां दिया गया है:

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

Improving memory usage

आदर्श रूप से, आप मेमोरी उपयोग को बेहतर बनाने के लिए हटाए गए आइटम (और उनके चिल्ड्रन!) को “table” ऑब्जेक्ट से हटा देंगे। यह वर्जन ऐसा करता है। यह Immer का भी उपयोग करता है अपडेट लॉजिक को अधिक संक्षिप्त बनाने के लिए।

import { useImmer } from 'use-immer';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, updatePlan] = useImmer(initialTravelPlan);

  function handleComplete(parentId, childId) {
    updatePlan(draft => {
      // Remove from the parent place's child IDs.
      const parent = draft[parentId];
      parent.childIds = parent.childIds
        .filter(id => id !== childId);

      // Forget this place and all its subtree.
      deleteAllChildren(childId);
      function deleteAllChildren(id) {
        const place = draft[id];
        place.childIds.forEach(deleteAllChildren);
        delete draft[id];
      }
    });
  }

  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 नेस्टिंग को कम कर सकते हैं। यह ephemeral UI state के लिए अच्छी तरह से काम करता है जिसे संग्रहीत करने की ज़रूरत नहीं होती है, जैसे कि कोई आइटम hovered है या नहीं।

Recap

  • अगर दो स्टेट वेरिएबल हमेशा एक साथ अपडेट होते हैं, तो उन्हें एक में मर्ज करने पर विचार करें।
  • “नामुमकिन” state को बनाने से बचने के लिए अपने state वेरिएबल्स सावधानी से चुनें।
  • अपने state को इस तरह से स्ट्रक्चर करें जिससे आपके द्वारा इसे अपडेट करने में गलती होने की संभावना कम हो जाए।
  • रिडेन्डेन्ट और डुप्लिकेट state से बचें ताकि आपको इसे सिंक में रखने की ज़रूरत न हो।
  • जब तक आप विशेष रूप से अपडेट को रोकना नहीं चाहते हैं, तब तक props को state में न रखें।
  • UI पैटर्न जैसे सिलेक्शन के लिए, ऑब्जेक्ट के बजाय ID या index को state में रखें।
  • यदि गहराई से नेस्टेड state को अपडेट करना कॉम्प्लेक्स है, तो इसे फ़्लैट करने का प्रयास करें।

Challenge of 4:

अपडेट न होने वाले कौम्पोनॅन्ट को ठीक करें

इस Clock कौम्पोनॅन्ट को दो prop मिलते हैं: color और time। जब आप सिलेक्ट बॉक्स में एक अलग रंग चुनते हैं, Clock कौम्पोनॅन्ट अपने पैरेंट कौम्पोनॅन्ट से एक अलग color prop प्राप्त करता है। हालाँकि, किसी कारण से, प्रदर्शित color अपडेट नहीं होता है। क्यों? समस्या हल करें।

import { useState } from 'react';

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