IlmHamroh
JavaScript Full-stack/17-qism. React asoslari44/45-dars19 daqiqa
Mundarija (27)

React custom hooklar II: useLocalStorage, useDebounce, useMediaQuery retseptlari va hook kutubxonalari

Qisqacha: Ko'p loyihalarda bir xil hooklar qayta-qayta yoziladi: useLocalStorage (state + xotira), useDebounce (yozish to'xtagach qiymat), useInterval, useOnClickOutside (tashqariga bosilsa yopish), useMediaQuery (ekran va tema), useEventListener (oyna hodisasi + cleanup). Ularning har biri 10–20 qator va siz bilgan hooklardan yig'iladi: useState, useEffect, useRef, useEffectEvent, useSyncExternalStore. Tayyor kutubxonalar ham bor — usehooks-ts, react-use, @uidotdev/usehooks, ahooks. Kutubxonani tanlashdan oldin hook kodini o'qing: u sizning talabingizga (tekshiruv, SSR, yangilanish) mos kelishi shart emas.

Bu darsda

  • Besh-olti mashhur hook retseptini o'zingiz yozasiz va qaysi hookdan qurilganini tushuntirasiz.
  • useEffectEvent bilan "obuna bir marta, ishlovchi har doim eng so'nggi" naqshini qo'llaysiz.
  • Tayyor hook kutubxonalarini haqiqiy raqamlar bilan taqqoslaysiz va manba kodini o'qib baholaysiz.
  • vazifalar da useOynaHodisasi va useMediaQuery ni yozib, hooklar qadamini yakunlaysiz.

Oldin bilishingiz kerak: Custom hooklar I, useEffectEvent, useSyncExternalStore, Debounce va throttle.

1. Nega bu kerak?

Oldingi darsda o'z hooklarimizni yozishni o'rgandik. Endi savol: qaysi hooklarni deyarli har loyihada yozasiz? Sardor «Bahor» saytida bir oyda shularni yozdi: tanlangan til va tema brauzerda saqlansin, qidiruv har harfda emas, yozish to'xtaganda ishlasin. Ochilgan menyu tashqariga bosilsa yopilsin, telefonda boshqa tartib chiqsin. Keyin bilsa — bularning hammasi tayyor kutubxonalarda bor ekan.

Bugun bu "retseptlar"ni o'zimiz yozamiz — 10–20 qatordan. Maqsad kutubxonani almashtirish emas, uni tushunish: hook ichida nima bor, qayerda tuzoq bor, kutubxona qachon kerak, qachon ortiqcha.

Hayotiy o'xshatish: oshxonada tayyor ziravorlar to'plami va o'zingiz yasagan aralashma. Tayyori qulay. Lekin oshpaz uning tarkibini biladi: tuzi ko'pmi, achchiqmi. Bilmasa, ovqatni buzib qo'yishi mumkin.

Bugungi retseptlar qaysi "asos" hooklardan yig'ilishi — bitta xaritada:

flowchart LR
  S["useState"] --> LS["useLocalStorage"]
  S --> DB["useDebounce"]
  E["useEffect + cleanup"] --> DB
  E --> EL["useEventListener"]
  E --> IN
  E --> OC
  EE["useEffectEvent"] --> IN["useInterval"]
  EE --> OC["useOnClickOutside"]
  EE --> EL
  X["useSyncExternalStore"] --> MQ["useMediaQuery"]

Chapda — siz bilgan hooklar, o'ngda — ulardan yasalgan retseptlar. Yangi narsa yo'q: har retsept 2–3 ta tanish bo'lakning birikmasi.

2. useLocalStorage: state + xotira

2.1 Retsept

Mehmon ismini kiritsa, sahifa qayta ochilganda ham eslab qolinsin (localStorage):

jsx
import { useState } from "react";

function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    try {
      const raw = localStorage.getItem(key);
      return raw === null ? initial : JSON.parse(raw);
    } catch {
      return initial; // JSON buzilgan yoki xotira yopiq
    }
  });

  function save(next) {
    setValue(next);
    try {
      localStorage.setItem(key, JSON.stringify(next));
    } catch {
      // xotira to'lgan yoki taqiqlangan — ekran baribir ishlaydi
    }
  }

  return [value, save];
}

function NameField() {
  const [name, setName] = useLocalStorage("bahor:ism", "");
  return (
    <p>
      <label>
        Ism{" "}
        <input value={name}
          onChange={(e) => setName(e.target.value)} />
      </label>{" "}
      Salom, {name || "mehmon"}!
    </p>
  );
}

function App() {
  const [visit, setVisit] = useState(0);
  return (
    <div>
      <NameField key={visit} />
      <button onClick={() => setVisit(visit + 1)}>
        Qayta ochish
      </button>
    </div>
  );
}

Ekranda:

text
Ism Salom, Malika!
Qayta ochish

«Qayta ochish» key ni o'zgartirib, NameField ni butunlay yangidan yaratadi (State'ning saqlanishi va key) — xuddi sahifa qayta ochilgandek. Yangi komponentning state'i localStorage dan tiklandi: "Malika".

Retsept tarkibi:

  • lazy useState — xotira faqat birinchi renderda o'qiladi (useState);
  • yozish — hodisada, effektda emas: "foydalanuvchi o'zgartirdi" — bu hodisa (Sizga effekt kerak emas);
  • try/catch — localStorage xato tashlashi mumkin: xotira to'lsa (QuotaExceededError), brauzer taqiqlasa, saqlangan matn JSON bo'lmasa.

2.2 Tuzoqlar va kanon qarori

Bu retseptning ojiz joyi — JSON.parse natijasi tekshirilmaydi. Kimdir DevTools'da qiymatni {"ism": 5} ga o'zgartirsa yoki ilovaning eski versiyasi boshqa shaklda saqlagan bo'lsa, komponent kutilmagan narsani oladi. Tayyor kutubxonalar ham odatda shunday: masalan, usehooks-ts 3.1.1 ning useLocalStorage i (paket manbasi) JSON.parse xato bersa default qiymatni qaytaradi va console.error ga yozadi. Lekin muvaffaqiyatli parse qilingan har qanday qiymatni tekshiruvsiz qabul qiladi.

vazifalar kanonida useLocalStorage yo'q — bu ataylab. Ro'yxat saqlash.ts orqali saqlanadi va o'qilganda paket.ts dagi to'liq tekshiruvdan o'tadi (versiya, har vazifaning shakli — 13–15-qismlarda yozilgan). localStorage ga faqat bitta modul tegadi. Umumiy useLocalStorage bu tekshiruvni chetlab o'tishga undardi.

Tekshirib ko'ring: Nega save ichida avval setValue, keyin localStorage.setItem? Teskari tartib xavfliroqmi?

Javob

setItem xato tashlashi mumkin (xotira to'lgan). Avval setValue — ekran baribir yangilanadi, foydalanuvchi yozgani yo'qolmaydi. Agar avval setItem bo'lib, u try siz xato tashlasa, setValue gacha yetmasdi va maydon "qotib" qolardi. Tartib bilan birga try/catch ham kerak.

3. useDebounce: yozish to'xtagach

Qidiruv har harfda serverga so'rov yubormasin — foydalanuvchi yozishni to'xtatgach, bir marta (Debounce va throttle):

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

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

function App() {
  const [query, setQuery] = useState("");
  const debounced = useDebounce(query, 300);

  useEffect(() => {
    if (debounced !== "") console.log("qidirildi:", debounced);
  }, [debounced]);

  return (
    <p>
      <input aria-label="Qidiruv" value={query}
        onChange={(e) => setQuery(e.target.value)} />{" "}
      Natija: {debounced}
    </p>
  );
}

Konsolda:

text
qidirildi: osh

Ekranda:

text
Natija: osh

Maydon ikki marta o'zgardi ("o", keyin "osh"), lekin "qidiruv" bir marta bo'ldi — oxirgi o'zgarishdan 300 ms keyin. Sir — effekt cleanup'ida (useEffect III): value o'zgarsa, eski taymer bekor bo'ladi va yangisi boshlanadi.

Vaqt query Taymer debounced
0 ms o 300 ms boshlandi ""
60 ms osh eskisi bekor, yangisi boshlandi ""
360 ms osh ishladi osh → effekt

vazifalar dagi qidiruvda debounce yo'q. Kanon izohida sababi yozilgan: qidiruv xotiradagi ro'yxatda, tarmoq va localStorage ga tegmaydi — har harfda darhol ishlasa ham tez (1 000 vazifada JS qismi 16–19 ms, Mini-ilova dagi o'lchov). Debounce — sekin yoki pullik ish oldidan (server so'rovi, og'ir hisob), har joyda emas.

4. useInterval va useEffectEvent

Taymerli hook'ning eski muammosi: callback har renderda yangi, lekin intervalni har renderda qayta boshlash noto'g'ri. useEffectEvent darsidagi yechim bu yerda ham ishlaydi:

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

function useInterval(callback, ms) {
  // tick — har doim eng so'nggi callback'ni chaqiradi
  const tick = useEffectEvent(callback);
  useEffect(() => {
    if (ms === null) return undefined; // null — to'xtatilgan
    const id = setInterval(() => tick(), ms);
    return () => clearInterval(id);
  }, [ms]);
}

function App() {
  const [orders, setOrders] = useState(0);
  // har 100 ms da yangi buyurtma, 3 tadan keyin to'xtaydi
  useInterval(() => setOrders(orders + 1), orders >= 3 ? null : 100);
  return <p>Yangi buyurtmalar: {orders}</p>;
}

Ekranda:

text
Yangi buyurtmalar: 3

Callback orders ni to'g'ridan-to'g'ri o'qiydi va eskirmaydi: useEffectEvent chaqiruv paytida eng so'nggi renderdagi funksiyani beradi. Effekt esa faqat ms ga bog'liq — interval keraksiz qayta boshlanmaydi. ms = null — "to'xtat" degan kelishuv, react.dev'dagi va kutubxonalardagi useInterval larda ham shunday.

5. useOnClickOutside: tashqariga bosilsa

Ochiladigan menyu, popover, modal — tashqariga bosilganda yopilishi kerak:

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

function useOnClickOutside(ref, handler) {
  const onOutside = useEffectEvent(handler);
  useEffect(() => {
    function listener(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        onOutside(event);
      }
    }
    document.addEventListener("click", listener);
    return () => document.removeEventListener("click", listener);
  }, [ref]);
}

function App() {
  const [open, setOpen] = useState(false);
  const box = useRef(null);
  useOnClickOutside(box, () => setOpen(false));
  return (
    <div>
      <span ref={box}>
        <button onClick={() => setOpen(!open)}>Menyu</button>
        {open && <ul><li>Osh</li><li>Manti</li></ul>}
      </span>
      <p><button>Boshqa joy</button></p>
      <p>Menyu: {open ? "ochiq" : "yopiq"}</p>
    </div>
  );
}

Ekranda:

text
Menyu
Boshqa joy
Menyu: yopiq

contains — bosilgan element qutining ichidami (DOM daraxti). Ichida bo'lsa — hech narsa, tashqarida — handler. «Menyu» tugmasi ham qutining ichida, shuning uchun uning o'z bosilishi menyuni darhol yopmaydi.

Tayyor kutubxonalarda bir farq bor: usehooks-ts sukut bo'yicha click emas, mousedown ni tinglaydi (paket manbasi, eventType = "mousedown"). Bunda menyu tugma qo'yib yuborilishini kutmasdan, bosilgan zahoti yopiladi. Bobur kabi klaviatura bilan ishlaydigan foydalanuvchi uchun esa bu yetmaydi: unga Escape va fokus chiqishi (focusout) ham kerak (Fokus boshqaruvi).

6. useMediaQuery va useEventListener

6.1 Media so'rovi — tashqi manba

CSS media so'rovi ((max-width: 600px), (prefers-color-scheme: dark)) JavaScript'da matchMedia bilan o'qiladi (matchMedia). Bu — tashqi manba: qiymat brauzerda yashaydi va change hodisasi bilan o'zgaradi. Demak to'g'ri vosita — useSyncExternalStore:

jsx
import { useSyncExternalStore } from "react";

// Har so'rovga bitta subscribe — barqaror
// (har renderda yangi bo'lmasin)
const subscribers = new Map();
function subscribeTo(query) {
  if (!subscribers.has(query)) {
    subscribers.set(query, (callback) => {
      const list = matchMedia(query);
      list.addEventListener("change", callback);
      return () => list.removeEventListener("change", callback);
    });
  }
  return subscribers.get(query);
}

function useMediaQuery(query) {
  return useSyncExternalStore(
    subscribeTo(query),
    () => matchMedia(query).matches,
    () => false, // serverda — taxmin
  );
}

function App() {
  const narrow = useMediaQuery("(max-width: 600px)");
  const dark = useMediaQuery("(prefers-color-scheme: dark)");
  return (
    <p>
      Ekran: {narrow ? "tor (telefon)" : "keng"}, tema:{" "}
      {dark ? "qorong'i" : "yorug'"}
    </p>
  );
}

Bu blokni saytda «Ishga tushir» bilan sinang va brauzer oynasini toraytirib ko'ring — matn o'zi o'zgaradi. (Tekshiruvchimiz ishlaydigan jsdom'da matchMedia yo'q, shuning uchun natija bu yerda yozilmagan.)

Ikki nozik joy:

  • subscribeTo(query) har so'rov uchun bitta funksiya qaytaradi va uni Map da saqlaydi. Agar subscribe ni hook ichida yozsak, har renderda yangi funksiya bo'lardi va React har safar qayta obuna bo'lardi — useSyncExternalStore dagi ikkinchi qoida.
  • usehooks-ts ning useMediaQuery i (3.1.1-versiya) boshqacha yozilgan: useState + layout effekt ichida matchMedia obunasi, eski brauzerlar uchun addListener bilan. Ishlaydi, lekin bu — useSyncExternalStore da ko'rgan "nusxa + effekt" usuli. Kutubxona kodini o'qish shuning uchun foydali: siz uning qaysi usulda yozilganini bilib tanlaysiz.

6.2 useEventListener

Oyna hodisasiga obuna — eng ko'p takrorlanadigan effekt: addEventListener + cleanup + ishlovchining eskirmasligi. Uni bitta hook'ga yig'amiz (kanondagi nomi — useOynaHodisasi, «Vazifalar qadami» da). Kanon varianti TypeScript'da, hodisa nomidan uning turini oladi:

tsx
import { useEffect, useEffectEvent, useState } from "react";

function useWindowEvent<T extends keyof WindowEventMap>(
  type: T,
  handler: (event: WindowEventMap[T]) => void,
): void {
  const call = useEffectEvent(handler);
  useEffect(() => {
    const listener = (event: WindowEventMap[T]) => call(event);
    addEventListener(type, listener);
    return () => removeEventListener(type, listener);
  }, [type]);
}

export default function App() {
  const [backs, setBacks] = useState(0);
  useWindowEvent("popstate", () => setBacks(backs + 1));
  const fakeBack = () => dispatchEvent(new PopStateEvent("popstate"));
  return (
    <div>
      <p>«Orqaga» bosildi: {backs}</p>
      <button onClick={fakeBack}>Orqaga (taqlid)</button>
    </div>
  );
}

Ekranda:

text
«Orqaga» bosildi: 1
Orqaga (taqlid)

WindowEventMap[T] — "popstate" uchun PopStateEvent, "online" uchun Event (keyof va indexed access). Ishlovchi backs ni to'g'ridan-to'g'ri o'qiydi — useEffectEvent tufayli eskirmaydi. Obuna esa faqat type o'zgarganda qayta ulanadi.

7. Tayyor hook kutubxonalari

7.1 Raqamlar

Mashhur to'rtta kutubxona — npm ma'lumotlari (2026-10-12/13, npm view va npm yuklab olishlar API'si):

Kutubxona Versiya (oxirgi chiqish) Litsenziya React peer
usehooks-ts 3.1.1 (2025-02-05) MIT ^16.8.0 … ^19
react-use 17.6.1 (2026-06-10) Unlicense *
@uidotdev/usehooks 2.4.1 (2026-05-14) MIT >=18.0.0
ahooks 3.10.0 (2026-09-20) MIT ^16.8.0 … ^19.0.0
Haftalik yuklab olishlar (npm, 2026-10-05 … 2026-10-11)
  • usehooks-ts6 189 5056 190 ming
  • react-use3 535 7133 536 ming
  • @uidotdev/usehooks1 408 9551 409 ming
  • ahooks975 368975 ming

Manba: api.npmjs.org/downloads/point/2026-10-05:2026-10-11/<paket>, 2026-10-13 da olindi

Grafik ommaboplikni ko'rsatadi, sifatni emas. Yuklab olishlarning katta qismi to'g'ridan-to'g'ri emas, boshqa paketlarning bog'liqligi sifatida keladi.

7.2 Har biri haqida

  • usehooks-ts — TypeScript'da yozilgan, 33 ta hook (paket eksportlari) (useLocalStorage, useDebounceValue, useMediaQuery, useOnClickOutside, useInterval, useIntersectionObserver, useEventListener …). Eng ko'p yuklanadi, lekin oxirgi versiyasi 2025-yil fevralida chiqqan. Bu xato emas, lekin "faol rivojlanyaptimi?" degan savolni beradi.
  • react-use — eng katta to'plam (17.6.1 paketining lib/ papkasida 111 ta use… fayli), ko'p yillik. Litsenziyasi — Unlicense (jamoat mulki).
  • @uidotdev/usehooks — ui.dev jamoasi; kichik va tushunarli kod, React 18+.
  • ahooks — Alibaba jamoasi; katta to'plam, so'rovlar uchun useRequest kabi og'irroq hooklar ham bor; hujjatlari xitoy va ingliz tilida.

Kutubxona hook'ini ishlatish ko'rinishi (o'z loyihangizda, npm i usehooks-ts):

tsx
import { useDebounceValue, useLocalStorage } from "usehooks-ts";

const [name, setName, removeName] = useLocalStorage("bahor:ism", "");
const [debounced, setValue] = useDebounceValue("", 300);

Turlar paketning index.d.ts faylidan: useLocalStorage<T>(key, initialValue, options?) uchta element qaytaradi — qiymat, o'rnatuvchi va o'chiruvchi. useDebounceValue esa qiymat va uni o'rnatuvchi (debounce qilingan) funksiyani.

7.3 Qanday tanlash

Savol Nimaga qarang
bitta-ikkita hook kerakmi? 15 qatorli retseptni o'zingiz yozing — bog'liqlik kamroq
kod sizning talabingizga mosmi? manbani o'qing: tekshiruv, try/catch, SSR, useSyncExternalStore
faol qo'llab-quvvatlanadimi? oxirgi chiqish sanasi, ochiq muammolar, React 19 peer'i
hajm faqat ishlatgan hook bundle'ga tushadimi (tree-shaking, bundle tahlili)
ta'minot zanjiri har yangi paket — yangi xavf (ta'minot zanjiri)

vazifalar kanoni kutubxonasiz qoldi: kerakli beshta hook jami ~110 qator, har biri loyihaning o'z talabiga moslangan.

8. Vazifalar qadami: useOynaHodisasi, useMediaQuery va commit

8.1 To'rtta bir xil effekt

Holat 42 dagi App.tsx da oyna hodisalari uchun to'rtta effekt bor edi — offline, online, popstate, pagehide. Har biri: funksiya, addEventListener, cleanup'da removeEventListener. online ishlovchisi uchun yana alohida useEffectEvent (useEffectEvent). O'sha refactor/hooklar branch'ida (Custom hooklar I dan davom) ularni bitta hook'ga yig'amiz.

src/hooklar/useOynaHodisasi.ts:

ts
// useOynaHodisasi.ts — window hodisasiga obuna (17/#44:
// "useEventListener" retsepti). App'da popstate, pagehide, online,
// offline uchun bir xil addEventListener + cleanup to'rt marta
// yozilgan edi
import { useEffect, useEffectEvent } from "react";

export function useOynaHodisasi<T extends keyof WindowEventMap>(
  tur: T,
  ishlovchi: (event: WindowEventMap[T]) => void,
): void {
  // Effekt hodisasi (#31): ishlovchi har renderda yangi funksiya
  // bo'lsa ham, obuna qayta ulanmaydi — chaqirilganda eng so'nggisi
  // ishlaydi
  const chaqir = useEffectEvent(ishlovchi);
  useEffect(() => {
    const tinglovchi = (event: WindowEventMap[T]) => chaqir(event);
    addEventListener(tur, tinglovchi);
    return () => removeEventListener(tur, tinglovchi);
  }, [tur]);
}

Izoh qatorlari telefonga sig'ishi uchun qayta bo'lindi (bu va keyingi fayllarda), matni o'sha. App.tsx da to'rtta effekt to'rtta qatorga aylandi:

diff
-  useEffect(() => {
-    const oflayn = () => sinxronniTox(xojalik);
-    const onlayn = () => onlaynBoldi();
-    addEventListener("offline", oflayn);
-    addEventListener("online", onlayn);
-    return () => {
-      removeEventListener("offline", oflayn);
-      removeEventListener("online", onlayn);
-    };
-  }, []);
 …
+  useOynaHodisasi("offline", () => sinxronniTox(xojalik));
+  // Ishlovchi ENG SO'NGGI vazifalarni o'qiydi, obuna esa bir marta:
+  // useOynaHodisasi ichida useEffectEvent (#31) — #28 da effekt
+  // [vazifalar] ga bog'liq edi va har o'zgarishda qayta ulanardi
+  useOynaHodisasi("online", () => {
+    void sinxronla(vazifalar, { dispatch, setTarmoq, xojalik });
+  });
+  useOynaHodisasi("popstate", () => dispatch({ tur: "tahrirYopildi" }));
+  useOynaHodisasi("pagehide", () => qoralamaniKechiktir.hoziroq());

onlaynBoldi effekt hodisasi ham o'chdi — u endi hook ichida.

8.2 useMediaQuery.ts va tema

Context II dagi TemaProvider tizim temasini useSyncExternalStore(tizimgaObuna, tizimQorongimi, () => false) bilan o'qirdi. Bu — umumiy retsept, shuning uchun hook'ga chiqdi:

ts
type Obuna = (tinglovchi: () => void) => () => void;

// subscribe funksiyasi har renderda YANGI bo'lsa, React har
// safar qayta obuna bo'ladi — har so'rovga bitta funksiya, modul
// darajasida
const obunalar = new Map<string, Obuna>();

function obunaniOl(sorov: string): Obuna {
  let obuna = obunalar.get(sorov);
  if (obuna === undefined) {
    obuna = (tinglovchi) => {
      const royxat = matchMedia(sorov);
      royxat.addEventListener("change", tinglovchi);
      return () => royxat.removeEventListener("change", tinglovchi);
    };
    obunalar.set(sorov, obuna);
  }
  return obuna;
}

export function useMediaQuery(sorov: string): boolean {
  return useSyncExternalStore(
    obunaniOl(sorov),
    () => matchMedia(sorov).matches,
    () => false,
  );
}

Bu — «Media so'rovi — tashqi manba» dagi retseptning o'zi, kanon nomlari bilan. TemaKonteksti.tsx da bir qator: const tizimQorongi = useMediaQuery(TIZIM_QORONGI); // custom hook (#44). tema-tanlash.ts dan tizimgaObuna va tizimQorongimi o'chdi, TIZIM_QORONGI eksport qilindi.

8.3 Testlar va natija

tekshiruv/hooklar.test.tsx — 4 ta test, har hook uchun bittadan (useOnlayn, useElon — oldingi darsda). useOynaHodisasi testi ikki muhim xulqni tekshiradi: obuna bir marta va chaqirilganda eng so'nggi ishlovchi:

tsx
test("bir marta ulanadi, chaqirilganda ENG SO'NGGI ishlovchi", () => {
  const ulash = vi.spyOn(window, "addEventListener");
  const chaqiruvlar: string[] = [];
  const { rerender, unmount } = renderHook(
    ({ nom }) =>
      useOynaHodisasi("popstate", () => chaqiruvlar.push(nom)),
    { initialProps: { nom: "birinchi" } },
  );
  rerender({ nom: "ikkinchi" });
  rerender({ nom: "uchinchi" });
  dispatchEvent(new PopStateEvent("popstate"));
  expect(chaqiruvlar).toEqual(["uchinchi"]);
  expect(
    ulash.mock.calls.filter(([tur]) => tur === "popstate"),
  ).toHaveLength(1);
  unmount();
  dispatchEvent(new PopStateEvent("popstate"));
  expect(chaqiruvlar).toEqual(["uchinchi"]); // cleanup ishladi
});

rerender — hook'ni yangi props bilan qayta chaqiradi (har safar yangi ishlovchi). useMediaQuery testida esa matchMedia soxta obyekt bilan almashtiriladi va change qo'lda chaqiriladi.

Haqiqiy natija (npm run test):

text
ℹ tests 156
ℹ pass 156
ℹ fail 0
 Test Files  5 passed (5)
      Tests  29 passed (29)

Vitest — 29 (+4), test fayllari — 5. Lint, format, tsc -b, knip — toza; brauzer tekshiruvi — 159/159. App.tsx — 590 qatordan 532 ga. Diff (ikki dars birga): 9 fayl, +229 −109.

Commit:

text
refactor: custom hooklar — useOnlayn, useMediaQuery, useElon…

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
localStorage dan kutilmagan shakl keldi JSON.parse natijasi tekshirilmagan o'qishda tekshiruv (sxema) yoki default
sahifa QuotaExceededError bilan yiqildi setItem try siz try/catch, ekran baribir yangilansin
debounce ishlamaydi, har harfda so'rov effektda cleanup yo'q return () => clearTimeout(timer)
interval eski qiymat bilan ishlaydi callback effekt ichida eskirgan useEffectEvent
interval har renderda qayta boshlanadi callback bog'liqliklarda useEffectEvent + faqat [ms]
useMediaQuery har renderda qayta obuna subscribe hook ichida yaratilgan har so'rovga bitta, modul darajasida
menyu o'z tugmasidan yopilib qoladi tugma ref qutisidan tashqarida tugmani ham qutiga kiriting

10. Mashqlar

Mashqlar kurs/mashqlar/17/44-hook-retseptlar/ da.

1-mashq (oson): useDebounce

Bo'sh joylarni to'ldiring:

jsx
function useDebounce(value, ms) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = [___:setTimeout](() => setDebounced(value), ms);
    return () => [___:clearTimeout](timer);
  }, [value, [___:ms]]);
  return debounced;
}
Yechim
jsx
import { useEffect, useState } from "react";

function useDebounce(value, ms) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), ms);
    return () => clearTimeout(timer);
  }, [value, ms]);
  return debounced;
}

const MENU = ["Osh", "Lag'mon", "Manti"];

function App() {
  const [text, setText] = useState("");
  const query = useDebounce(text, 150).toLowerCase();
  const found = MENU.filter((d) => d.toLowerCase().includes(query));
  return (
    <div>
      <input aria-label="Taom" value={text}
        onChange={(e) => setText(e.target.value)} />
      <p>Topildi: {found.join(", ")}</p>
    </div>
  );
}

Ekranda:

text
Topildi: Lag'mon

ms ham bog'liqlik — muddat props'dan kelib o'zgarsa, taymer yangi muddat bilan qayta boshlanadi.

2-mashq (o'rta): tekshiruvli useLocalStorage

TypeScript'da useStoredState<T>(key, initial, isValid) yozing. U darsdagi useLocalStorage ga o'xshaydi, lekin o'qilgan qiymat isValid(qiymat) dan o'tmasa, initial qaytadi. isValid — type guard (Type guard'lar): (v: unknown) => v is T. «Bahor» uchun: saqlangan mehmonlar soni 1–12 oralig'idagi butun son bo'lsin. Avval localStorage ga buzilgan qiymat ("ko'p") yozib, hook default qiymat berishini ko'rsating.

Yechim
tsx
import { useState } from "react";

export function useStoredState<T>(
  key: string,
  initial: T,
  isValid: (v: unknown) => v is T,
): [T, (next: T) => void] {
  const [value, setValue] = useState<T>(() => {
    try {
      const raw = localStorage.getItem(key);
      if (raw === null) return initial;
      const parsed: unknown = JSON.parse(raw);
      return isValid(parsed) ? parsed : initial;
    } catch {
      return initial;
    }
  });
  function save(next: T) {
    setValue(next);
    try {
      localStorage.setItem(key, JSON.stringify(next));
    } catch {
      // xotira yopiq — ekran baribir ishlaydi
    }
  }
  return [value, save];
}

const isGuests = (v: unknown): v is number =>
  typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= 12;

localStorage.setItem("bahor:mehmonlar", JSON.stringify("ko'p"));

export default function App() {
  const [guests, setGuests] = useStoredState(
    "bahor:mehmonlar",
    2,
    isGuests,
  );
  return (
    <p>
      Mehmonlar: {guests}{" "}
      <button onClick={() => setGuests(Math.min(guests + 1, 12))}>
        +1
      </button>
    </p>
  );
}

Ekranda:

text
Mehmonlar: 3 +1

Saqlangan "ko'p" — JSON sifatida to'g'ri, lekin son emas. isGuests uni rad etdi va hook 2 dan boshladi, keyin «+1» — 3. unknown — JSON.parse natijasini tekshirmasdan ishlatishga TypeScript ruxsat bermaydi (unknown). Kanondagi paket.ts ham xuddi shu g'oya — kattaroq shaklda.

3-mashq (qiyin): Escape va tashqariga bosish

useDismiss(ref, onDismiss) hook'ini yozing — menyuni ikki yo'l bilan yopadi: qutidan tashqariga bosish (click) va Escape tugmasi (keydown, event.key === "Escape", Klaviatura hodisalari). Talablar: ikkala tinglovchi ham document da, ishlovchi eskirmasin (useEffectEvent), komponent yopilsa ikkalasi ham olib tashlansin. Menyu yopilganda fokus «Menyu» tugmasiga qaytsin — Bobur klaviaturada adashmasin.

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

export function useDismiss(ref, onDismiss) {
  const dismiss = useEffectEvent(onDismiss);
  useEffect(() => {
    function onClick(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        dismiss();
      }
    }
    function onKey(event) {
      if (event.key === "Escape") dismiss();
    }
    document.addEventListener("click", onClick);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("click", onClick);
      document.removeEventListener("keydown", onKey);
    };
  }, [ref]);
}

function App() {
  const [open, setOpen] = useState(false);
  const box = useRef(null);
  const trigger = useRef(null);
  useDismiss(box, () => {
    if (!open) return;
    setOpen(false);
    trigger.current.focus();
  });
  return (
    <div>
      <span ref={box}>
        <button ref={trigger} onClick={() => setOpen(!open)}>
          Menyu
        </button>
        {open && <ul><li>Osh</li><li>Manti</li></ul>}
      </span>
      <p><button>Boshqa joy</button></p>
      <p>Menyu: {open ? "ochiq" : "yopiq"}</p>
    </div>
  );
}

Ekranda:

text
Menyu
Boshqa joy
Menyu: yopiq

Ishlovchi open ni o'qiydi — useEffectEvent tufayli har doim eng so'nggisini. if (!open) return — menyu yopiq bo'lsa, har bosishda fokusni "o'g'irlamaslik" uchun. Escape'ni sinash: «Ishga tushir» dan keyin menyuni oching va Esc'ni bosing.

11. Real ishda

  • Har loyihada src/hooks/ da shu retseptlarning bir nechtasi bor; ularning sifati — tekshiruv, try/catch, cleanup — kod ko'rigida birinchi qaraladigan narsa.
  • Kutubxona tanlash — kasbiy qaror: "faqat useLocalStorage uchun 30 hookli paket kerakmi?", "u React 19 bilan sinalganmi?", "SSR'da ishlaydimi?". Bunday savollarni bugungi jadval bilan berasiz.
  • UI kutubxonalari (Radix, Headless UI, React Aria) "tashqariga bosish", Escape, fokus qaytarish kabi xulqni o'zi beradi — ular ichida xuddi shunday hooklar (Headless komponentlar).
  • Intervyu savollari: "useDebounce ni yozib bering", "useInterval da callback nega eskiradi va qanday tuzatiladi?", "useMediaQuery ni qaysi hook bilan yozasiz va nega?", "useLocalStorage ning qanday xavflari bor?".

Xulosa

  • Retseptlar — siz bilgan hooklardan yig'iladi: useState + xotira, useEffect + cleanup, useEffectEvent, useSyncExternalStore.
  • useLocalStorage — lazy o'qish, hodisada yozish, try/catch va (iloji bo'lsa) tekshiruv.
  • useDebounce — cleanup'da eski taymerni bekor qilish; useInterval/useEventListener — obuna bir marta, ishlovchi useEffectEvent orqali.
  • useMediaQuery — tashqi manba: useSyncExternalStore, har so'rovga bitta subscribe.
  • usehooks-ts, react-use, @uidotdev/usehooks, ahooks — tanlashdan oldin kodini, sanasini va peer'ini tekshiring.
  • vazifalar: useOynaHodisasi va useMediaQuery bilan beshta hook, 4 ta test; App.tsx 590 → 532 qator.

Keyingi dars: React ilovani debug qilish — xato xabarini, component stack'ni va React DevTools'ni o'qib, xatoni tez topamiz va 17-qismni yakunlaymiz.

Manbalar

  • React: "Reusing Logic with Custom Hooks" — react.dev/learn/reusing-logic-with-custom-hooks
  • React: useEffectEvent — react.dev/reference/react/useEffectEvent
  • usehooks-ts 3.1.1 (npm paketi: dist/index.d.ts, dist/index.js) — usehooks-ts.com
  • react-use 17.6.1, @uidotdev/usehooks 2.4.1, ahooks 3.10.0 — npm (2026-10-13); yuklab olishlar — api.npmjs.org
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React custom hooklar II: useLocalStorage, useDebounce, useMediaQuery retseptlari va hook kutubxonalari — IlmHamroh