IlmHamroh
JavaScript Full-stack/20-qism. React: UI kutubxonalar va animatsiya20/30-dars21 daqiqa
Mundarija (29)

Kichik React kutubxonalari: autosize, OTP, drawer, debounce, nanoid — o'zim yozamanmi yoki o'rnatamanmi?

Qisqacha: Kichik kutubxona kichik muammoni hal qiladi — lekin har paket hajm, CSP xavfi va kimningdir xizmatiga bog'liqlik demak. Avval brauzerda tayyori bormi, tekshiring: crypto.randomUUID(), structuredClone, Object.groupBy, CSS field-sizing. Yo'q bo'lsa — mantiq oddiymi (debounce — 10 qator) yoki chekka holatlar ko'pmi (OTP, drawer)? O'lchab qaror qiling: masalan butun lodash +25.6 KB, es-toolkit dan bitta funksiya +0.3 KB, input-otp esa qattiq CSP'da <style> xatosini beradi.

Bu darsda

  • "O'zim yozamanmi yoki o'rnatamanmi?" savoliga beshta mezon bilan javob berasiz.
  • react-textarea-autosize, input-otp, vaul va react-remove-scroll nima qilishini va ularning narxini bilasiz.
  • use-debounce, es-toolkit, nanoid, ts-pattern va dequal o'rniga qachon o'z kodingiz yoki brauzer API'si yetishini ajratasiz.
  • e18e tashabbusi va "native" muqobillar ro'yxatidan foydalanasiz.
  • Kutubxonani o'lchashni o'rganasiz: gzip hajm va CSP ostida ochib ko'rish.

Oldin bilishingiz kerak: Command palette va klaviatura yorliqlari, Og'ir ishni UI'dan ajratish: debounce, throttle va Web Worker, Sayoz va chuqur nusxalash, Discriminated union va to'liqlik tekshiruvi.

1. Nega bu kerak?

Oldingi darsda katta kutubxonalarni (React Flow, Konva) o'lchadik. Bugun teskarisi — kichiklari.

Sardor bir kunda «Bahor» buyurtma formasiga oltita paket qo'shdi: izoh maydoni o'ssin, SMS kod chiroyli bo'lsin, mobil filtr pastdan chiqsin, qidiruv kechiksin, buyurtma raqami noyob bo'lsin, ikki obyekt solishtirilsin. Har biri "atigi bir necha kilobayt".

Jasur aka package.json ga qarab so'radi: "Har paketni kim yangilaydi? Ertaga muallifi tashlab ketsa-chi? Va nega SMS kod maydoni konsolda qizil xato chiqaryapti?"

Hayotiy o'xshatish: oshxona jihozlari. Qozon kerak — sotib olasiz, o'zingiz quymaysiz. Lekin har sabzini to'g'rash uchun alohida mashina olsangiz, oshxona mashinalarga to'lib ketadi. Ularni tozalash, ta'mirlash ham sizda. Pichoq bilan bir daqiqalik ish uchun mashina kerak emas.

Bu darsda har kichik kutubxonani o'lchaymiz va bitta savolga javob beramiz: o'zim yozamanmi yoki o'rnatamanmi?

2. Qaror mezonlari

2.1 Besh savol

Har paketdan oldin besh savol:

  1. Brauzerda (yoki Node'da) tayyori bormi? Bo'lsa — paket kerak emas. Bugun ko'p mashhur paketlarning o'rnini platforma egalladi.
  2. Mantiq qanchalik oddiy? 10–20 qator, chekka holatlari kam — o'zingiz yozasiz va test qilasiz.
  3. Chekka holatlar va a11y ko'pmi? Mobil brauzerlar, ekran o'quvchi, SMS avto-to'ldirish, sensor ekran — bu yerda tajribali kutubxona yillar davomida yig'gan tuzatishlar qimmat turadi.
  4. Narxi qancha? gzip hajm (birinchi yuklashga qo'shiladimi?) va bog'liqliklar soni.
  5. Xavfsizlik siyosatiga mosmi va kim qo'llab-quvvatlaydi? CSP ostida ishlaydimi, oxirgi reliz qachon bo'lgan, muammolarga javob beriladimi?
flowchart TD
  A{"Platformada bormi?"} -->|ha| B["Platforma API"]
  A -->|yo'q| C{"Mantiq oddiymi?"}
  C -->|ha| D["O'zingiz yozing + test"]
  C -->|yo'q| E{"CSP va hajm mosmi?"}
  E -->|ha| F["Kutubxona, aniq versiya"]
  E -->|yo'q| G["Boshqa kutubxona yoki o'z yechim"]

Sxemada nimaga qarang: kutubxona — oxirgi qadam, birinchisi emas. Lekin "o'zim yozaman" ham avtomatik javob emas: chekka holatlar ko'p bo'lsa, o'z kodingiz ham qarzga aylanadi.

2.2 Qanday o'lchadik

Kanon tajribasidagi usulni (UI kutubxonalar xaritasi darsidagi) shu dars uchun takrorladik. Har kutubxonaga alohida kichik sahifa: React + bitta komponent. Vite 8.3.3 bilan build, JS fayllar gzip bilan siqildi va faqat React'li "asos" sahifadan farqi olindi. Keyin sahifa vazifalar CSP'si ostida Chrome'da ochildi, maydon bo'lsa — unga yozildi. Natija va har sahifaning manba kodi kanon tajribasida saqlandi (natijalar/tajriba/20-kichik-kutubxonalar.txt, Chrome 155.0.8059.82, 2026-10-17).

Versiyalarni aniq qotirdik (npm i -E) va faqat kamida 7 kunlik relizlarni oldik. Versiyalar: react-textarea-autosize 8.5.9, input-otp 1.5.0, react-remove-scroll 2.7.2, use-debounce 10.1.1, es-toolkit 1.52.0, lodash va lodash-es 4.18.1, nanoid 6.0.2, ts-pattern 5.9.0, dequal 2.0.3. ts-pattern'ning 5.9.2 versiyasi 3 kunlik edi — shuning uchun 5.9.0.

Tekshirib ko'ring: Paketning npm sahifasida "2 KB" yozilgan. Bu raqamga ishonib qo'yaversa bo'ladimi?

Javob

Yo'q. Bu raqam boshqa sharoitda o'lchangan: siqilganmi yoki yo'qmi, butun paketmi yoki bitta funksiyami — noma'lum. Sizning build'ingizda tree-shaking qancha kesadi, qaysi bog'liqliklar allaqachon bor — bular natijani o'zgartiradi. O'z ilovangizda (yoki shunga yaqin kichik sahifada) o'lchang — kanon shunday qiladi.

3. Formalar uchun

3.1 O'sadigan izoh maydoni: react-textarea-autosize

Buyurtma izohi: "Piyozsiz", "3-qavat, domofon ishlamaydi". Ba'zan bir so'z, ba'zan uch qator. Oddiy <textarea> qat'iy balandlikda: kam yozilsa — bo'sh joy, ko'p yozilsa — ichida aylantirish (scroll).

react-textarea-autosize — matnga qarab o'sadigan <textarea>:

jsx
import TextareaAutosize from "react-textarea-autosize";

<label>
  Izoh
  <TextareaAutosize minRows={2} maxRows={6} />
</label>

O'lchov: +1.5 KB, CSP 0. Biz yozib ko'rdik: 2 qatorlik maydon (36 px) 5 qator yozilgach 81 px ga o'sdi. U balandlikni JavaScript bilan o'lchaydi va style xususiyatiga yozadi — CSP buni to'smaydi.

Endi platformaga qaraymiz. CSS'da yangi xususiyat bor: field-sizing: content — maydon o'lchami mazmunga moslashadi. Bir qator CSS, JavaScript'siz:

html
<style>
  body { font-family: system-ui, sans-serif; }
  textarea {
    field-sizing: content;
    min-height: 3lh;
    max-height: 8lh;
    width: 16rem;
    padding: 0.5rem;
    font: inherit;
  }
</style>
<label>
  Izoh — yozing, maydon o'sadi:<br>
  <textarea>Osh, 2 porsiya.
Piyozsiz bo'lsin.</textarea>
</label>

Natija oynasida maydonga yangi qatorlar yozing — u max-height gacha o'sadi, keyin aylantirish paydo bo'ladi. lh — bitta qator balandligi: 3lh — kamida uch qator.

Lekin field-sizing hali hamma brauzerda yo'q: Chrome va Edge'da (123-versiyadan) bor, boshqalarida — caniuse va Baseline bilan tekshiring. Qo'llab-quvvatlamaydigan brauzerda maydon oddiy <textarea> kabi ishlaydi. Hech narsa buzilmaydi, faqat o'smaydi. Bu progressiv yaxshilash: hamma uchun ishlaydi, yangi brauzerda — chiroyliroq.

Qaror: izoh maydoni uchun field-sizing yetadi. Hamma brauzerda o'sishi shart bo'lsa — react-textarea-autosize (+1.5 KB, CSP 0) yaxshi tanlov.

3.2 SMS kod: input-otp

Bank ilovalaridagidek 6 ta katakcha. Chiroyli ko'rinadi, lekin ichida tuzoqlar ko'p:

  • telefon SMS'dan kodni o'zi taklif qiladi (avto-to'ldirish) — u bitta maydonga keladi;
  • foydalanuvchi kodni nusxalab qo'yadi (paste) — 6 raqam birdan;
  • Backspace orqaga yurishi, ekran o'quvchi kodni bitta maydon deb o'qishi kerak.

Ko'p jamoalar 6 ta alohida <input> qiladi va shu tuzoqlarga tushadi. To'g'ri yo'l — bitta haqiqiy maydon va uning ustida 6 ta "rasm". Avval o'zimiz yozamiz:

jsx
import { useState } from "react";

export default function App() {
  const [code, setCode] = useState("");
  const left = 6 - code.length;
  return (
    <div>
      <label>
        SMS kod{" "}
        <input
          inputMode="numeric"
          autoComplete="one-time-code"
          maxLength={6}
          value={code}
          onChange={(e) =>
            setCode(e.target.value.replace(/\D/g, "").slice(0, 6))
          }
        />
      </label>
      <p aria-hidden="true">
        {Array.from({ length: 6 }, (_, i) => (
          <span key={i}>{code[i] ?? "·"} </span>
        ))}
      </p>
      <p>
        {left === 0 ? "Tekshirilmoqda…" : `${left} ta raqam qoldi`}
      </p>
    </div>
  );
}

Ekranda:

text
SMS kod
4 8 2 1 3 ·
1 ta raqam qoldi

"48a213" yozildi — "a" tashlandi (replace(/\D/g, "") — raqam bo'lmagan hamma belgini o'chiradi, regex darsidan). Muhim atributlar:

  • inputMode="numeric" — telefonda raqamli klaviatura chiqadi.
  • autoComplete="one-time-code" — brauzer va telefon "bu bir martalik kod" deb biladi va SMS'dagi kodni taklif qiladi.
  • aria-hidden katakchalar — ular faqat ko'z uchun. Malika bitta maydonni eshitadi: "SMS kod, tahrirlash maydoni".

Katakchalarni chiroyli qilish (haqiqiy maydonni shaffof qilib ustiga qo'yish, kursor qaysi katakda ekanini ko'rsatish) — yana ish. input-otp kutubxonasi aynan shuni beradi. shadcn'ning InputOTP komponenti ham shu kutubxona ustiga qurilgan. Registrdagi input-otp.tsx OTPInput ni aynan input-otp dan import qiladi:

jsx
import { OTPInput } from "input-otp";

<OTPInput
  maxLength={6}
  aria-label="SMS kod"
  render={({ slots }) => (
    <div className="otp">
      {slots.map((slot, i) => (
        <span key={i} data-active={slot.isActive}>
          {slot.char ?? "·"}
        </span>
      ))}
    </div>
  )}
/>

O'lchov: +3.7 KB. CSP bilan esa kutilmagan natija chiqdi:

text
otp           JS  71500 (+  3.7 KB) | <style> 1 | CSP style-src-elem×1 | xato: Applying inline style violates the following Content Security Policy directive 'style-src 

input-otp sahifaga o'z <style> tegini qo'shmoqchi bo'ldi. vazifalar CSP'si (style-src 'self') uni to'sdi va konsolda xato chiqdi. Radix Primitives chuqur darsidagi modal menyu bilan bir xil holat. Maydonning o'zi ishladi: biz "123456" yozdik, qiymat ham, katakchalar ham to'g'ri. Lekin kutubxona qo'shmoqchi bo'lgan stil yo'q.

Qaror: kodni bir marta kiritadigan oddiy ekran uchun o'z maydoningiz (yuqoridagi 20 qator) yetadi. Dizayn murakkab bo'lsa — input-otp, lekin qattiq CSP'li loyihada konsoldagi xatoni va yo'qolgan stilni hisobga oling.

Tekshirib ko'ring: Nega 6 ta alohida <input> SMS avto-to'ldirish bilan yomon ishlaydi?

Javob

Telefon SMS'dagi kodni bitta maydonga, bitta qiymat sifatida qo'yadi: "482130". 6 ta maydonda u faqat birinchisiga tushadi — yoki birinchi raqam qoladi, yoki hammasi bitta katakka sig'maydi. Buni tuzatish uchun har maydonda paste va input hodisalarini ushlab, raqamlarni qo'shnilarga taqsimlash kerak. Bitta maydon bilan bu muammo umuman yo'q.

4. Oynalar va scroll

4.1 Mobil drawer: vaul

vaul — telefonda pastdan chiqadigan, barmoq bilan surib yopiladigan drawer. Uni Modal, drawer, popover va tooltip darsida batafsil ko'rdik: +21.4 KB va vazifalar CSP'si ostida uchta <style> buzilishi. Bu darsning besh savoli nuqtai nazaridan yana bir narsa — beshinchi savol (kim qo'llab-quvvatlaydi): vaul'ning oxirgi versiyasi 1.1.2 — 2024-yil 14-dekabrda chiqqan (npm ma'lumoti, 2026-10-17 da tekshirildi). Bu "yomon" degani emas — kutubxona tayyor bo'lishi ham mumkin. Lekin xato topsangiz, tuzatishni kutish qiyin bo'lishi mumkin.

Muqobil — native <dialog> va CSS: oyna pastga yopishgan, @starting-style bilan pastdan chiqadi (CSS animatsiyalari React'da darsida yozamiz). Barmoq bilan surib yopish bo'lmaydi, lekin "Yopish" tugmasi, Esc va fon bosish ishlaydi. vazifalar dagi tasdiq oynasi shunday.

4.2 Fonni qotirish: react-remove-scroll

Oyna ochiq turganda orqadagi sahifa aylanmasin (scroll lock). Buni Radix ichida react-remove-scroll qiladi. O'lchov: +4.0 KB va yana <style>:

text
removescroll  JS  71811 (+  4.0 KB) | <style> 1 | CSP style-src-elem×1 | xato: Applying inline style violates the following Content Security Policy directive 'style-src 

Radix Primitives chuqur darsida modal menyu CSP'ni buzganining sababi aynan shu kutubxona edi. Endi u yakka o'zi o'lchandi.

Muqobil — bir qator CSS, :has() bilan:

css
body:has(dialog[open]) {
  overflow: hidden;
}

"Ichida ochiq <dialog> bor body" — aylanmaydi. Bu qoida sizning .css faylingizda — CSP'ga mos. react-remove-scroll esa ko'proq ish qiladi: iOS Safari'dagi chekka holatlar, scrollbar yo'qolganda sahifa "sakramasligi", ichki aylanadigan bloklar. Kerak bo'lsa — buni bilib tanlaysiz.

5. Mantiq uchun yordamchilar

5.1 Debounce: o'z hook'ingiz yoki use-debounce

Debounce'ni Og'ir ishni UI'dan ajratish darsida o'tganmiz: yozish to'xtagach, bir marta. React'da eng qulay shakli — qiymatni kechiktiradigan hook:

jsx
import { useEffect, useState } from "react";

function useDebouncedValue(value, ms) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), ms);
    return () => clearTimeout(t); // yangi harf — eski taymer bekor
  }, [value, ms]);
  return debounced;
}

export default function App() {
  const [q, setQ] = useState("");
  const dq = useDebouncedValue(q, 300);
  useEffect(() => {
    if (dq) console.log("so'rov:", dq);
  }, [dq]);
  return (
    <div>
      <input
        aria-label="Qidiruv"
        value={q}
        onChange={(e) => setQ(e.target.value)}
      />
      <p>Natija: {dq || "—"}</p>
    </div>
  );
}

Konsolda:

text
so'rov: osh

Uch harf yozildi, so'rov — bitta. Harflar yozilayotganda «Natija: —» hali bo'sh turadi. 300 ms jimlikdan keyin "osh" keldi. Effektning tozalash funksiyasi (useEffect II) har yangi harfda eski taymerni bekor qiladi.

use-debounce paketi shu narsani va ko'proq imkoniyatni beradi: useDebounce (qiymat), useDebouncedCallback (funksiya), leading, maxWait, flush, cancel. O'lchov: +0.9 KB, CSP 0. Bular kerak bo'lsa — yaxshi tanlov.

Kanon nima qildi? vazifalar da o'z kechiktir funksiyasi bor (taymer.ts, 13-qismdan beri). Unda hoziroq() va bekor() ham bor — sahifa yopilayotganda qoralamani darhol saqlash uchun:

ts
export function kechiktir<A extends unknown[]>(
  ishlovchi: (...argumentlar: A) => void,
  kutish: number,
): Kechiktirilgan<A> {
  let taymer: ReturnType<typeof setTimeout> | undefined;
  // kutayotgan chaqiruv argumentlari
  let kutayotgan: A | null = null;

  function hoziroq(): void {
    clearTimeout(taymer);
    if (kutayotgan === null) {
      return;
    }
    const argumentlar = kutayotgan;
    kutayotgan = null;
    ishlovchi(...argumentlar);
  }

  function bekor(): void {
    clearTimeout(taymer);
    kutayotgan = null;
  }

  function kechiktirilgan(...argumentlar: A): void {
    kutayotgan = argumentlar;
    clearTimeout(taymer);
    taymer = setTimeout(hoziroq, kutish);
  }

  return Object.assign(kechiktirilgan, { hoziroq, bekor });
}

30 qator, testlari bilan. Paket kerak bo'lmadi.

5.2 es-toolkit va lodash

lodash — JavaScript'ning eng mashhur yordamchi kutubxonasi: debounce, groupBy, cloneDeep, chunk va yuzlab boshqa funksiya. U ES modullardan oldin yozilgan, shuning uchun import _ from "lodash" hammasini olib keladi. es-toolkit — zamonaviy muqobil: faqat ES modullar, TypeScript turlari ichida, ishlatmagan funksiya build'ga tushmaydi (tree-shaking). lodash'dan ko'chish uchun es-toolkit/compat moduli ham bor.

Bir xil sahifa, bir xil debounce, besh yo'l:

Bitta debounce narxi: JS gzip (React ustiga)
  • lodash (butun)25,6 KB
  • lodash-es1 KB
  • use-debounce0,9 KB
  • es-toolkit0,3 KB
  • o'z funksiya0,2 KB

Manba: IlmHamroh kurs tajribasi — kichik kutubxonalar o'lchovi (Vite 8.3.3, gzip -9, Chrome 155, 2026-10-17)

Grafikda nimaga qarang: farq kutubxonada emas, import usulida. lodash dan _.debounce — 25.6 KB, chunki butun kutubxona keldi. lodash-es dan nomli import — 1.0 KB. Eski loyihada import _ from "lodash" ni ko'rsangiz — birinchi tekshiradigan joy shu.

js
import _ from "lodash"; // ❌ hammasi (+25.6 KB)
import { debounce } from "lodash-es"; // ✅ faqat kerakli
import { debounce } from "es-toolkit"; // ✅ zamonaviy

5.3 Noyob id: nanoid yoki crypto.randomUUID()

Buyurtmaga noyob id kerak. nanoid — qisqa, URL'ga xavfsiz id yasaydi: 21 belgi, A–Z a–z 0–9 _ -. O'lchov: +0.2 KB.

js
import { customAlphabet, nanoid } from "nanoid";

nanoid(); // masalan "V1StGXR8_Z5jdHi6B-myT" — 21 belgi
const orderNumber = customAlphabet("0123456789", 6);
orderNumber(); // masalan "482130" — 6 ta raqam

Brauzer va Node'da esa crypto.randomUUID() bor — 0 KB, 36 belgilik standart UUID. Kanonda aynan u ishlatiladi (saqlash.ts: server kaliti, jonli-manba.ts: tab belgisi). nanoid qachon kerak? Id URL'da ko'rinadi va qisqa bo'lishi kerak (/b/V1StGXR8), yoki o'z alifbongiz kerak (faqat raqam — buyurtma raqami).

Diqqat: crypto.randomUUID() faqat xavfsiz kontekstda ishlaydi: HTTPS yoki localhost. Oddiy http:// sahifada u yo'q — sahifani lokal tarmoqdagi IP orqali (http://192.168…) ochib telefonda sinasangiz, crypto.randomUUID is not a function xatosini ko'rasiz.

5.4 ts-pattern va dequal

ts-pattern — holat bo'yicha tanlashning (switch) kuchliroq shakli. Har variantni yozganingizni TypeScript tekshiradi:

ts
import { match, P } from "ts-pattern";

const text = match(state)
  .with({ tur: "yuklanmoqda" }, () => "Yuklanmoqda…")
  .with({ tur: "tayyor", soni: P.number }, (s) => `${s.soni} ta`)
  .with({ tur: "xato" }, (s) => s.xabar)
  .exhaustive();

.exhaustive() — "hamma holat yozildi". Bittasini unutsangiz — tip xatosi. O'lchov: +2.3 KB. Biz Node'da ham sinadik: { tur: "tayyor", soni: 3 } uchun "3 ta buyurtma" qaytdi. Turi noto'g'ri qiymat kelsa, exhaustive() ishlash paytida ham xato tashlaydi: Pattern matching error: no pattern matches value {"tur":"nomalum"}.

Oddiy holatda esa TypeScript'ning o'zi yetadi — Discriminated union darsidagi never tekshiruvi. «xato» holatini unutdik:

tsx
type Holat =
  | { tur: "yuklanmoqda" }
  | { tur: "tayyor"; soni: number }
  | { tur: "xato"; xabar: string };

function matn(h: Holat): string {
  switch (h.tur) {
    case "yuklanmoqda":
      return "Yuklanmoqda…";
    case "tayyor":
      return `${h.soni} ta buyurtma`;
    default: {
      const tekshir: never = h;
      return tekshir;
    }
  }
}

Xato:

text
TS2322: Type '{ tur: "xato"; xabar: string; }' is not assignable to type 'never'.

Tarjimasi: "{ tur: "xato"; xabar: string; } turini never ga berib bo'lmaydi". default ga «xato» holati yetib keldi — demak u yozilmagan. 0 KB. ts-pattern ichma-ich shakllar, bir nechta maydon bo'yicha tanlash kerak bo'lganda kuchli. Bitta tur maydoni bo'yicha esa switch + never yetadi.

dequal — ikki qiymatni "chuqur" solishtiradi: ichidagi hamma narsa tengmi. O'lchov: +0.4 KB. Qachon kerak? Masalan, serverdan kelgan sozlama o'zgarmagan bo'lsa, qayta saqlamaslik uchun. JSON.stringify bilan solishtirish tuzog'ini pastdagi misolda ko'rasiz.

6. Platformaning o'zi: native muqobillar

6.1 Bir blokda to'rtta muqobil

Bugun ko'p mashhur paketlarning ishini til va brauzer o'zi qiladi. Bu blok Node 24 da ishlaydi (brauzerda ham xuddi shunday):

js
const id = crypto.randomUUID();
console.log(id.length, id[14]);
const order = { taom: "osh", vaqt: new Date(2026, 9, 17), soni: [2] };
const copy = structuredClone(order);
copy.soni.push(1);
console.log(order.soni, copy.vaqt instanceof Date);
const viaJson = JSON.parse(JSON.stringify(order));
console.log(typeof viaJson.vaqt);
const menu = [
  { nom: "osh", tur: "taom" },
  { nom: "choy", tur: "ichimlik" },
  { nom: "manti", tur: "taom" },
];
console.log(Object.groupBy(menu, (m) => m.tur));
console.log(
  JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 }),
);

Konsolda:

text
36 4
[ 2 ] true
string
[Object: null prototype] {
  taom: [ { nom: 'osh', tur: 'taom' }, { nom: 'manti', tur: 'taom' } ],
  ichimlik: [ { nom: 'choy', tur: 'ichimlik' } ]
}
false

Qatorma-qator:

  1. 36 4 — UUID 36 belgi, 15-belgisi (id[14]) doim 4: bu "4-versiya, tasodifiy" belgisi. uuid paketi kerak emas.
  2. [ 2 ] true — structuredClone chuqur nusxa oldi: nusxaga qo'shilgan 1 asl massivga tegmadi. Date esa Date bo'lib qoldi. lodash.cloneDeep kerak emas.
  3. string — JSON orqali "nusxa" sanani satrga aylantirib yubordi. Bu eski usulning tuzog'i.
  4. Object.groupBy — guruhlash (Guruhlash darsida). [Object: null prototype] — natija obyektining prototipi yo'q, shuning uchun toString kabi meros kalitlar bilan to'qnashuv bo'lmaydi. Kanonning royxat.ts ida statistika shu bilan hisoblanadi.
  5. false — kalitlari bir xil, tartibi boshqa ikki obyekt JSON.stringify da teng emas. Chuqur solishtirish uchun dequal (yoki o'z kichik funksiyangiz) shu sababli kerak.

6.2 e18e: ekotizimni yengillashtirish

e18e (ecosystem performance — "ekotizim tezligi"; 18 — birinchi "e" va oxirgi "e" orasidagi harflar soni) — JavaScript paketlarini yengilroq va tezroq qilish tashabbusi (e18e.dev). Uning module-replacements paketi — "qaysi paketni nima bilan almashtirish mumkin" ro'yxati. Ro'yxat uch faylga bo'lingan: native (platformaning o'zi), micro-utilities (bir necha qatorlik kod bilan almashadigan mayda paketlar) va preferred (afzal ko'rilgan muqobil paket). eslint-plugin-depend — shu ro'yxatni o'qiydigan ESLint plagini: package.json yoki importda ro'yxatdagi paket bo'lsa, ogohlantiradi.

Ro'yxatdan bir nechta juft (module-replacements 3.4.0, 2026-10-17 da o'qildi):

Paket Ro'yxatdagi muqobil Ro'yxat Qaysi darsda
uuid crypto.randomUUID() preferred shu dars
clone-deep structuredClone() native Sayoz va chuqur nusxalash
lodash.groupby Object.groupBy() native Guruhlash
lodash es-toolkit preferred shu dars
moment day.js, date-fns, luxon, Date, Temporal preferred Intl.DateTimeFormat

Qoida: yangi paket qo'shishdan oldin "platformada bormi?" deb e18e ro'yxatidan qidiring.

Tekshirib ko'ring: Hamkasbingiz "structuredClone bor-ku, endi JSON.parse(JSON.stringify(x)) ham yomon emas, ikkalasi bir xil" dedi. To'g'rimi?

Javob

Yo'q. Blokdagi uchinchi qatorga qarang: JSON orqali Date satrga aylandi (string). undefined maydonlar yo'qoladi, Map/Set bo'sh obyektga aylanadi, aylana havola xato beradi. structuredClone bularning hammasini to'g'ri nusxalaydi. Faqat funksiya va DOM elementini nusxalay olmaydi — u holda xato tashlaydi.

7. Kanon tajribasi: vazifalar qanday tanladi

Bu dars uchun vazifalar ga qadam yo'q — kanon bu savollarga oldingi qismlarda javob bergan:

Vazifa Kanonda Paket o'rniga
debounce taymer.ts → kechiktir (+hoziroq, bekor) use-debounce, lodash
noyob id crypto.randomUUID() nanoid, uuid
guruhlash Object.groupBy (royxat.ts) lodash
tasdiq oynasi native <dialog> vaul, Radix Dialog

package.json dagi har ish paketi — o'lchab olingan qaror: Radix menyu, cmdk, dnd-kit, Motion. Kichik ishlar esa platformada. Bugungi o'lchov ham shuni tasdiqladi: kichik kutubxonalarning ko'pi hajmda arzon (0.2–4 KB). Lekin ikkitasi (input-otp, react-remove-scroll) va vaul vazifalar CSP'si ostida <style> xatosi beradi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
bitta funksiya uchun +25 KB import _ from "lodash" lodash-es yoki es-toolkit dan nomli import
crypto.randomUUID is not a function http:// (xavfsiz kontekst emas) HTTPS yoki localhost
SMS kod telefonda o'zi to'lmaydi autoComplete="one-time-code" yo'q yoki 6 ta maydon bitta maydon + atribut
Applying inline style violates … 'style-src 'self'' kutubxona <style> qo'shadi (input-otp, remove-scroll, vaul) CSS faylda o'z yechim yoki boshqa kutubxona
JSON.stringify bilan teng obyektlar "teng emas" kalitlar tartibi boshqa dequal yoki o'z deepEqual
debounce'li qidiruv eski natijani ko'rsatadi taymer tozalanmagan effekt tozalashida clearTimeout

9. Mashqlar

Mashqlar kurs/mashqlar/20/20-kichik-kutubxonalar/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): o'rnatamizmi?

Har vaziyat uchun yozing: platforma, o'zim yoki kutubxona.

  • Buyurtmaga noyob id kerak, URL'da ko'rinmaydi →
  • Qidiruvga 300 ms debounce, boshqa hech narsa kerak emas →
  • Mobil ilovadagidek drawer: barmoq bilan surib yopish, tezlikka qarab "otilib" yopilish →
  • Obyektni sanasi bilan birga chuqur nusxalash →
Yechim
  • Id — crypto.randomUUID() (platforma).
  • 300 ms debounce — 10 qatorlik hook (o'zim).
  • Surib yopiladigan drawer — sensor ekran fizikasi, chekka holatlar ko'p (kutubxona, masalan vaul). Lekin CSP o'lchovini unutmang: vaul <style> qo'shadi.
  • Chuqur nusxa — structuredClone (platforma).

2-mashq (o'rta): telefon raqami maydoni

+998 90 000 00 00 ko'rinishidagi maydonni kutubxonasiz yozing. Foydalanuvchi faqat 9 ta raqam yozadi, ekranda esa +998 90 123 45 67 formatida chiqadi.

Talablar: type="tel", inputMode="numeric", autoComplete="tel-national". Raqam bo'lmagan belgilar tashlanadi. 9 tadan ortig'i qabul qilinmaydi. Maydon ostida formatlangan raqam chiqsin.

Ishora: SMS kod maydonidagi replace(/\D/g, "").slice(…). Format uchun raqamlarni 2, 3, 2 va 2 talik bo'laklarga ajrating (slice bilan), keyin bo'shliq bilan qo'shing.

Yechim
jsx
import { useState } from "react";

function formatPhone(digits) {
  const parts = [
    digits.slice(0, 2),
    digits.slice(2, 5),
    digits.slice(5, 7),
    digits.slice(7, 9),
  ].filter(Boolean);
  return ["+998", ...parts].join(" ");
}

export default function App() {
  const [digits, setDigits] = useState("");
  return (
    <div>
      <label>
        Telefon{" "}
        <input
          type="tel"
          inputMode="numeric"
          autoComplete="tel-national"
          value={digits}
          onChange={(e) =>
            setDigits(e.target.value.replace(/\D/g, "").slice(0, 9))
          }
        />
      </label>
      <p>{formatPhone(digits)}</p>
    </div>
  );
}

Ekranda:

text
Telefon
+998 90 123 45 67

"90-123-45-67-8" yozildi: chiziqlar tashlandi, oxirgi "8" — o'ninchi raqam, slice(0, 9) uni kesdi. filter(Boolean) bo'sh bo'laklarni olib tashlaydi: hali 4 raqam yozilganda natija +998 90 12 bo'ladi, oxirida ortiqcha bo'shliqlar qolmaydi. Haqiqiy loyihada raqamni server ham tekshiradi — klient formati faqat qulaylik uchun.

3-mashq (qiyin): useDebouncedCallback ni o'zingiz yozing

use-debounce'dagi useDebouncedCallback ning soddalashtirilgan nusxasi:

  1. useDebouncedCallback(fn, ms) — chaqirilsa, ms jimlikdan keyin fn ni oxirgi argumentlar bilan bir marta bajaradigan funksiya qaytaradi.
  2. Qaytgan funksiya har renderda bir xil bo'lsin (useRef yoki useMemo).
  3. fn har renderda yangi bo'lsa ham, eng oxirgisi chaqirilsin (eski closure tuzog'i — useEffectEvent darsidagi muammo).
  4. Komponent o'chirilsa — kutayotgan taymer bekor qilinsin.

Tekshirish: «+» tugmasi uch marta tez bosilsa, konsolda bitta saqlandi: 3 chiqsin.

Ishora: oxirgi fn ni ref'da saqlang va har renderda yangilang. Taymerni ham ref'da saqlang.

Yechim
jsx
import { useEffect, useMemo, useRef, useState } from "react";

function useDebouncedCallback(fn, ms) {
  const fnRef = useRef(fn);
  const timer = useRef();
  useEffect(() => {
    fnRef.current = fn; // eng oxirgi fn — eski closure emas
  });
  useEffect(() => () => clearTimeout(timer.current), []);
  return useMemo(
    () =>
      (...args) => {
        clearTimeout(timer.current);
        timer.current = setTimeout(
          () => fnRef.current(...args),
          ms,
        );
      },
    [ms],
  );
}

export default function App() {
  const [count, setCount] = useState(0);
  const save = useDebouncedCallback(() => {
    console.log("saqlandi:", count);
  }, 300);
  useEffect(() => {
    if (count > 0) save();
  }, [count, save]);
  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
      <p>Porsiya: {count}</p>
    </div>
  );
}

Konsolda:

text
saqlandi: 3

Ekranda:

text
+
Porsiya: 3

Uchta bosish — uchta save(), lekin har biri oldingi taymerni bekor qildi. 300 ms dan keyin faqat oxirgisi bajarildi va u oxirgi fn ni chaqirdi — count 3. Agar fnRef.current = fn qatorini olib tashlasangiz, birinchi renderdagi closure ishlaydi va konsolda saqlandi: 0 chiqadi. save useMemo tufayli har renderda bir xil — effekt bog'liqliklarida turishi xavfsiz.

4-mashq: o'z loyihangiz auditi

Portfolio loyihangizning package.json ini oching. Har dependencies paketi uchun jadval tuzing: nima uchun kerak, platformada muqobili bormi (e18e ro'yxati), taxminiy hajmi (npm run build dan oldin va keyin). Bitta paketni platforma API'si bilan almashtiring.

Yechim

Tekshiruv ro'yxati:

  • Har paketning "nega" ustuni bitta gap bilan to'ldirilgan.
  • uuid, lodash.*, moment, classnames kabi paketlar bo'lsa — muqobili yozilgan (crypto.randomUUID, nomli import yoki platforma, Intl, clsx).
  • Almashtirishdan keyin build hajmi o'lchangan (oldin/keyin, gzip) va testlar o'tgan.
  • package.json da versiyalar aniq (^ siz) — semver darsidagi qoida.

10. Real ishda

  • Kod ko'rigi (code review): "nega yangi paket?" — eng ko'p beriladigan savollardan. Javob: o'lchov va beshta mezon.
  • Xavfsizlik: har paket — ta'minot zanjirining bir halqasi. npm'da buzilgan yoki o'g'irlangan paketlar orqali hujumlar bo'lgan. Paketlar kamroq bo'lsa, hujum yuzasi ham kichikroq.
  • Bank va davlat loyihalari: qattiq CSP — <style> qo'shadigan kutubxonalar birinchi tekshiruvdan o'tmaydi.
  • Intervyu savollari: "lodash'ni qanday to'g'ri import qilasiz?", "structuredClone va JSON.parse(JSON.stringify()) farqi?", "debounce hook'ini yozing", "SMS kod maydonini qanday qilasiz?".

Xulosa

  • Paketdan oldin besh savol: platformada bormi, mantiq oddiymi, chekka holatlar ko'pmi, narxi qancha, CSP va xizmat holati.
  • Formalar: field-sizing: content (yoki react-textarea-autosize, +1.5 KB); OTP — bitta maydon, one-time-code (input-otp +3.7 KB, <style> CSP'da to'siladi).
  • Oynalar: vaul +21.4 KB va CSP 3; react-remove-scroll +4.0 KB va CSP 1; muqobil — <dialog> va body:has(dialog[open]).
  • Mantiq: debounce — o'z hook'ingiz; lodash — faqat nomli import; id — crypto.randomUUID(); never — ts-pattern'siz to'liqlik tekshiruvi.
  • e18e module-replacements — "platformada bormi?" savolining ro'yxati.

Keyingi dars: UI animatsiya tamoyillari va performance — animatsiya modulini boshlaymiz: davomiylik, easing, spring va nega faqat transform hamda opacity arzon.

Manbalar

  • react-textarea-autosize, input-otp, vaul, use-debounce, es-toolkit, nanoid, ts-pattern, dequal — npm va GitHub sahifalari (versiyalar — «Qanday o'lchadik» bo'limida)
  • shadcn/ui registri: input-otp.tsx — github.com/shadcn-ui/ui
  • MDN: field-sizing, crypto.randomUUID(), structuredClone(), Object.groupBy() — developer.mozilla.org
  • e18e — e18e.dev; module-replacements — github.com/e18e/module-replacements; eslint-plugin-depend — github.com/es-tooling/eslint-plugin-depend
  • MDN: autocomplete atributi (one-time-code, tel-national) — developer.mozilla.org
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Kichik React kutubxonalari: autosize, OTP, drawer, debounce, nanoid — o'zim yozamanmi yoki o'rnatamanmi? — IlmHamroh