React.memo’ni chuqurroq o’rganish

React.memo qanday ishlashini qisqacha tushuntirib o’tamiz. React’da yangilanish yuz berganda, sizning komponentingiz avvalgi renderdan qaytarilgan vDOM(virtual DOM) natijasi bilan taqqoslanadi. Agar bu natijalar boshqacha bo’lsa — ya’ni, props o’zgarsa — reconciler host muhiti (odatda brauzer DOM) ichida mavjud bo’lgan element uchun yangilash effektini ishlatadi yoki u mavjud bo’lmasa, joylashtirish effektini bajaradi. Agar props bir xil bo’lsa, komponent baribir qayta render qilinadi va DOM ham yangilanadi.

React.memo’ning asosiy afzalligi shundaki, u komponentning props qiymatlari har ikki render o’rtasida bir xil bo’lsa, kerak bo’lmagan qayta render qilishlardan qochishga yordam beradi. React’da buni qila olishimiz mumkin ekan, savol tug’iladi: qanchalik va qaysi holatlarda memoizatsiya qilishimiz kerak? Agar har bir komponentni memoizatsiya qilsak, umuman olganda ilovamiz tezroq bo’larmikin?

Memoizatsiya qilingan komponentlar baribir qayta render bo’lishi

React.memo komponentning props qiymatlari o’zgarganmi yoki yo’qligini aniqlash uchun yuzaki taqqoslash (shallow comparison) usulidan foydalanadi. Bu yondashuvning kamchiligi shundaki, JavaScript’da skalyar turlar aniq taqqoslanishi mumkin bo’lsa-da, skalyar bo’lmagan turlar taqqoslashda aniq natija bermaydi. Bularni yaxshiroq tushunib olishimiz uchun, keling, avval skalyar va skalyar bo’lmagan turlar nima ekanligini hamda ularning taqqoslash operatsiyalaridagi xatti-harakatlarini ko’rib chiqamiz.

Skalyar turlar (primitive types)

Skalyar turlar, shuningdek, primitiv turlar deb ham ataladi, va ular asosiy, sodda qiymatlarni ifodalaydi. Bu turlar yakka, bo’linmas qiymatlarni ifodalaydi va array’lar yoki object’lar kabi murakkab ma’lumot strukturalariga ega emaslar. Skalyar turlar tabiatan o’zgarmas (immutable) hisoblanadi, ya’ni bir marta belgilanganidan so’ng, yangi qiymat yaratmasdan turib o’zgartirilishi mumkin emas. JavaScript’da bir necha skalyar turlar mavjud, jumladan, number, string, boolean, va boshqalar, masalan, symbol, BigInt, undefined, va null. Har biri o’ziga xos maqsadga xizmat qiladi. Misol uchun, number turlari o’z-o’zidan tushunarli bo’lsa, symbol qiymatlari takrorlanmas identifikatorlar yaratish uchun ishlatiladi, undefined va null esa turli kontekstlarda qiymatning yo’qligini ifodalash imkonini beradi. Skalyar qiymatlar taqqoslanayotganda, biz odatda ularning haqiqiy qiymatini yoki tarkibini taqqoslaymiz.

Skalyar bo’lmagan turlar (reference types)

Skalyarlarning soddaligidan tashqariga chiqsak, biz skalyar bo’lmagan yoki referens(reference) yoki havola turlari bilan tanishamiz. Bu turlar ma’lumotlarni o’zida saqlamaydi, balki ma’lumotlar xotirada saqlanadigan joyga ishora qiluvchi havola yoki ko’rsatkichni saqlaydi. Bu farqni anglash juda muhim, chunki bu turlar qanday taqqoslanishi, boshqarilishi va kodda o’zaro ishlashi bilan bog’liq xatti-harakatlariga ta’sir qiladi. JavaScript’da eng keng tarqalgan skalyar bo’lmagan turlar object’lar va array’lar hisoblanadi. Object’lar kalit-qiymat juftliklari bilan tuzilgan ma’lumotlarni saqlashga imkon beradi, array’lar esa tartiblangan to’plamni taqdim etadi. Funksiyalar ham JavaScript’da referens turlari hisoblanadi.

Skalyar bo’lmagan turlarning asosiy xususiyati shundaki, bir nechta havola bir xil xotira joyiga ishora qilishi mumkin. Bu shuni anglatadiki, agar ma’lumot bir havola orqali o’zgartirilsa, bu o’zgarish boshqa bir xil ma’lumotga ishora qiluvchi havolalarga ham ta’sir qiladi. Taqqoslashga kelganda, skalyar bo’lmagan turlar xotiradagi havolasi orqali taqqoslanadi, tarkibi orqali emas, skalyar turlardan farqli o’laroq. Bu ba’zida ushbu nozik jihat bilan tanish bo’lmagan odamlar uchun kutilmagan natijalarga olib kelishi mumkin. Masalan, tarkibi bir xil bo’lgan ikkita array, lekin xotirada turli joylarda saqlanayotgan bo’lsa, ular qat’iy tenglik operatori yordamida taqqoslanganda teng bo’lmaydi.

// Skalyar turlar
"a" === "a"; // string; true
3 === 3; // number; true
 
// Skalyar bo'lmagan turlar
[1, 2, 3] === [1, 2, 3]; // array; false
{ foo: "bar" } === { foo: "bar" } // object; false

Yuqoridagi massivlarni taqqoslashda, array’lar, object’lar va boshqa skalyar bo’lmagan turlar havola, ya’ni reference, orqali taqqoslanadi: ya’ni chap tarafdagi array’ning kompyuter xotirasidagi joyiga bo’lgan havolasi o’ng tarafdagi array’ning xotiradagi joyiga tengmi yoki yo’qmi, shuni solishtiradi. Shuning uchun taqqoslash false qiymatini qaytaradi. Object’lar uchun ham xuddi shunday. Obyektlarni taqqoslaganimizda, xotirada chap taraf va o’ng tarafda ikkita turli obyekt yaratamiz — tabiiyki, ular teng emas, ular xotirada ikki xil joyda yashaydigan ikkita turli obyekt! Ularning kontenti bir xil bo’lsa ham.

React.memo’dan foydalanishda e’tibor qilish kerak bo’lgan jihatlar

React.memo ishlatishda ba’zi bir chalg’ituvchi jihatlar bo’lishi mumkin. Quyidagi misol orqali buni ko’rib chiqamiz: List deb nomlangan funksional komponentga items nomli massivni props sifatida uzatamiz va uni render qilamiz:

const List = React.memo(function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
});

Endi, bu komponentni ota komponent ichida ishlatamiz va har bir ota komponent qayta render qilinganda yangi massiv nusxasini uzatamiz:

function ParentComponent({ allFruits }) {
  const [count, setCount] = React.useState(0);
  const favoriteFruits = allFruits.filter((fruit) => fruit.isFavorite);
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <List items={favoriteFruits} />
    </div>
  );
}

Har safar Increment tugmasi bosilganda, ParentComponent qayta render qilinadi. Garchi List komponentiga uzatilayotgan items massivining qiymati o’zgarmagan bo’lsa ham, har safar qayta ['apple', 'banana', 'cherry'] kabi yangi massiv nusxasi yaratiladi. React.memo props’ni yuzaki taqqoslash (shallow comparison) orqali taqqoslaydi, shuning uchun bu yangi massiv nusxasini oldingi renderdagi massivdan farqli deb ko’radi va List komponentini keraksiz qayta render qiladi.

Bu muammoni hal qilish uchun, massivni useMemo yordamida memoizatsiya qilishimiz mumkin:

function ParentComponent({ allFruits }) {
  const [count, setCount] = React.useState(0);
  const favoriteFruits = React.useMemo(
    () => allFruits.filter((fruit) => fruit.isFavorite),
    []
  );
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <List items={favoriteFruits} />
    </div>
  );
}

Endi massiv faqat bir marta yaratiladi va har bir qayta render qilinishda bir xil havolani saqlaydi, bu esa List komponentining keraksiz qayta render qilinishining oldini oladi.

Bu misol React.memo va skalyar bo’lmagan, ya’ni referens turlar bilan, props’lar bilan ishlaganda havola bo’yicha taqqoslashni tushunish qanchalik muhimligini ko’rsatadi. Agar ehtiyotkorlik bilan foydalanilmasa, optimallashtirish o’rniga, tasodifan ishlash samaradorligiga salbiy ta’sir qiluvchi muammolarni yuzaga keltirishi mumkin.

Funksiyalarni memoizatsiya qilish

React.memo ko’pincha funksiyalar kabi referens turdagi qiymatlar tufayli ham chetlab o’tiladi. Quyidagi holatni ko’rib chiqamiz:

<MemoizedAvatar
  name="Tejas"
  url="https://github.com/tejasq.png"
  onChange={() => save()}
/>

Yuqoridagi kodda props’lar o’zgarmaydigandek tuyuladi: name, url, va onChange o’zgarmas(constant) qiymatlarga ega. Ammo props’larni taqqoslasak, quyidagicha bo’ladi:

"Tejas" === "Tejas"; // <- `name` prop; true
"https://github.com/tejasq.png" === "https://github.com/tejasq.png"; // <- `url` prop; true
(() => save()) === (() => save()); // <- `onChange` prop; false

Buning sababi shundaki, funksiyalar havola orqali taqqoslanadi. Har safar qayta render qilinganda, onChange prop uchun yangi funksional nusxa yaratiladi va bu yangi havola(reference) deb hisoblanadi, natijada MemoizedAvatar komponenti qayta memoizatsiya qilinmaydi.

Bu muammoni useCallback hook’idan foydalanib hal qilish mumkin. useCallback yordamida ota komponent ichida funksiyani xotirada saqlab qo’yamiz, shunda u faqatgina uning qaramlik(dependency) ro’yxatidagi biror narsa o’zgarganda yangilanadi:

const Parent = ({ currentUser }) => {
  const onAvatarChange = useCallback(
    (newAvatarUrl) => {
      updateUserModel({ avatarUrl: newAvatarUrl, id: currentUser.id });
    },
    [currentUser]
  );
 
  return (
    <MemoizedAvatar
      name="Tejas"
      url="https://github.com/tejasq.png"
      onChange={onAvatarChange}
    />
  );
};

Endi ishonchimiz komil bo’lishi mumkinki, onAvatarChange hech qachon o’zgarmaydi, agar uning qaramliklar massividagi(ikkinchi argumenti) biror narsa o’zgarmasa. Shu orqali, biz qilgan memoizatsiya texnikasi to’liq va ishonchli bo’ladi. Bu funksiyalarni props orqali komponentga o’tkazganda tavsiya etiladigan usul hisoblanadi.

Ajoyib! Ammo bu hali ham barcha holatlarda memoizatsiya qilinadigan komponentlar keraksiz ravishda qayta render qilinmasligini anglatadi, shundaymi yoki yo’qmi? Biz yana bir narsani inobatga olishimiz kerak bo’ladi, u haqida keyingi mavzuda batafsil to’xtalamiz.