Mundarija (27)
- Bu darsda
- 1. Nega bu kerak?
- 2. useLocalStorage: state + xotira
- 2.1 Retsept
- 2.2 Tuzoqlar va kanon qarori
- 3. useDebounce: yozish to'xtagach
- 4. useInterval va useEffectEvent
- 5. useOnClickOutside: tashqariga bosilsa
- 6. useMediaQuery va useEventListener
- 6.1 Media so'rovi — tashqi manba
- 6.2 useEventListener
- 7. Tayyor hook kutubxonalari
- 7.1 Raqamlar
- 7.2 Har biri haqida
- 7.3 Qanday tanlash
- 8. Vazifalar qadami: useOynaHodisasi, useMediaQuery va commit
- 8.1 To'rtta bir xil effekt
- 8.2 useMediaQuery.ts va tema
- 8.3 Testlar va natija
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): useDebounce
- 2-mashq (o'rta): tekshiruvli useLocalStorage
- 3-mashq (qiyin): Escape va tashqariga bosish
- 11. Real ishda
- Xulosa
- Manbalar
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.
useEffectEventbilan "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.
vazifalardauseOynaHodisasivauseMediaQueryni 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):
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:
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—localStoragexato 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
saveichida avvalsetValue, keyinlocalStorage.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):
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:
qidirildi: oshEkranda:
Natija: oshMaydon 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:
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:
Yangi buyurtmalar: 3Callback 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:
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:
Menyu
Boshqa joy
Menyu: yopiqcontains — 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:
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 uniMapda saqlaydi. Agarsubscribeni hook ichida yozsak, har renderda yangi funksiya bo'lardi va React har safar qayta obuna bo'lardi — useSyncExternalStore dagi ikkinchi qoida.usehooks-tsninguseMediaQueryi (3.1.1-versiya) boshqacha yozilgan:useState+ layout effekt ichidamatchMediaobunasi, eski brauzerlar uchunaddListenerbilan. 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:
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:
«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 |
- 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 tause…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
useRequestkabi og'irroq hooklar ham bor; hujjatlari xitoy va ingliz tilida.
Kutubxona hook'ini ishlatish ko'rinishi (o'z loyihangizda, npm i usehooks-ts):
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:
// 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:
- 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:
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:
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):
ℹ 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:
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:
function useDebounce(value, ms) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = [___:setTimeout](() => setDebounced(value), ms);
return () => [___:clearTimeout](timer);
}, [value, [___:ms]]);
return debounced;
}Yechim
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:
Topildi: Lag'monms 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
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:
Mehmonlar: 3 +1Saqlangan "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
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:
Menyu
Boshqa joy
Menyu: yopiqIshlovchi 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
useLocalStorageuchun 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: "
useDebounceni yozib bering", "useIntervalda callback nega eskiradi va qanday tuzatiladi?", "useMediaQueryni qaysi hook bilan yozasiz va nega?", "useLocalStoragening qanday xavflari bor?".
Xulosa
- Retseptlar — siz bilgan hooklardan yig'iladi:
useState+ xotira,useEffect+ cleanup,useEffectEvent,useSyncExternalStore. useLocalStorage— lazy o'qish, hodisada yozish,try/catchva (iloji bo'lsa) tekshiruv.useDebounce— cleanup'da eski taymerni bekor qilish;useInterval/useEventListener— obuna bir marta, ishlovchiuseEffectEventorqali.useMediaQuery— tashqi manba:useSyncExternalStore, har so'rovga bittasubscribe.- usehooks-ts, react-use, @uidotdev/usehooks, ahooks — tanlashdan oldin kodini, sanasini va peer'ini tekshiring.
vazifalar:useOynaHodisasivauseMediaQuerybilan beshta hook, 4 ta test;App.tsx590 → 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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!