Hooklar
@akhatmirzo/hooks — 27 ta bog'liqliksiz, SSR'da xavfsiz React hook.
Komponentlar ichida qayta-qayta yoziladigan mantiq: kechiktirish, localStorage, media
so'rovlar, klaviatura yorliqlari, sahifalash. Paket faqat react ga tayanadi (18 va undan
yangi), serverda xato bermaydi va @akhatmirzo/ui siz ham ishlaydi.
pnpm add @akhatmirzo/hooksimport { useDebounce, useHotkeys, useLocalStorage } from '@akhatmirzo/hooks';| Hook | Vazifasi |
|---|---|
useToggle | true / false holat va almashtirgich |
useDebounce | Qiymatni kechiktirish (qidiruv) |
useDebouncedCallback | Funksiyani kechiktirish (avtomatik saqlash) |
useControllableState | Boshqariladigan va boshqarilmaydigan holat |
useLocalStorage | localStorage bilan sinxron holat |
useMediaQuery | CSS media so'rov natijasi |
useClickOutside | Element tashqarisiga bosish |
useCopyToClipboard | Buferga nusxalash |
useIntersection | Ko'rinish maydoniga kirish (cheksiz scroll) |
useEventListener | Tiplangan hodisa tinglovchisi |
useResizeObserver | Element o'lchamini kuzatish |
useScrollLock | Sahifa scroll'ini qulflash (modal) |
useFocusTrap | Fokusni konteyner ichida ushlash |
useLongPress | Uzoq bosish |
useDocumentVisibility | Tab ko'rinib turibdimi |
useIsClient | Gidratatsiyadan keyin true |
useAsync | Asinxron funksiya holati |
usePagination | Sahifalash tugmalari |
useHotkeys | Klaviatura yorliqlari |
useInterval | Interval (null — to'xtaydi) |
useTimeout | Bir martalik taymer |
useCountdown | Teskari sanoq (SMS kod) |
useIdle | Foydalanuvchi faol emasligi |
useStep | Ko'p bosqichli jarayon (wizard) |
useInfiniteScroll | Cheksiz scroll |
usePrevious | Oldingi qiymat |
useOnline | Tarmoq holati |
Next.js'da
Hooklar faqat client komponentlarda ishlaydi — faylning birinchi qatoriga 'use client'; qo'ying.
Holat
useToggle
function useToggle(
initial?: boolean,
): readonly [value: boolean, toggle: () => void, setValue: Dispatch<SetStateAction<boolean>>];Mantiqiy holat. toggle barqaror (har render'da bir xil), shuning uchun uni to'g'ridan-to'g'ri
onClick ga yoki boshqa hooklarga berish mumkin. initial — standart false.
'use client';
import { useToggle } from '@akhatmirzo/hooks';
import { Button, Switch } from '@akhatmirzo/ui';
export default function HooksToggle() {
const [notify, toggleNotify, setNotify] = useToggle(true);
return (
<div className="flex flex-col items-center gap-4">
<Switch
checked={notify}
onCheckedChange={setNotify}
label="SMS xabarnomalar"
description={notify ? 'Yoqilgan' : "O'chirilgan"}
/>
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={toggleNotify}>
Almashtirish
</Button>
<Button variant="ghost" onClick={() => setNotify(false)}>
O'chirish
</Button>
</div>
</div>
);
}
const [open, toggleOpen, setOpen] = useToggle();
<Button onClick={toggleOpen}>Filtrlar</Button>;
<Button onClick={() => setOpen(false)}>Yopish</Button>;useControllableState
function useControllableState<T>(params: {
value?: T;
defaultValue?: T;
onChange?: (value: T) => void;
}): [T, Dispatch<SetStateAction<T>>];O'z komponentingizni ham boshqariladigan (value + onValueChange), ham
boshqarilmaydigan (defaultValue) qilish uchun. value !== undefined bo'lsa — qiymat
otadan keladi, aks holda ichki holat ishlatiladi. onChange ikkala rejimda ham, faqat qiymat
haqiqatan o'zgarganda chaqiriladi. Kutubxonadagi Dialog, Tabs, Select shu tamoyilda
ishlaydi.
Boshqarilmaydigan (defaultValue=3)
Boshqariladigan (0…20)
'use client';
import { useControllableState } from '@akhatmirzo/hooks';
import { Button } from '@akhatmirzo/ui';
import { useState } from 'react';
interface StepperProps {
value?: number;
defaultValue?: number;
onValueChange?: (value: number) => void;
}
function Stepper({ value, defaultValue = 0, onValueChange }: StepperProps) {
const [count, setCount] = useControllableState({ value, defaultValue, onChange: onValueChange });
return (
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" onClick={() => setCount((n) => n - 1)}>
−
</Button>
<span className="min-w-8 text-center font-semibold text-text tabular-nums">{count}</span>
<Button size="sm" variant="outline" onClick={() => setCount((n) => n + 1)}>
+
</Button>
</div>
);
}
export default function HooksControllableState() {
const [value, setValue] = useState(10);
return (
<div className="grid w-full max-w-md gap-6 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<p className="text-xs font-medium text-text-muted">Boshqarilmaydigan (defaultValue=3)</p>
<Stepper defaultValue={3} />
</div>
<div className="flex flex-col gap-2">
<p className="text-xs font-medium text-text-muted">Boshqariladigan (0…20)</p>
<Stepper value={value} onValueChange={(v) => setValue(Math.min(20, Math.max(0, v)))} />
</div>
</div>
);
}
interface StepperProps {
value?: number;
defaultValue?: number;
onValueChange?: (value: number) => void;
}
function Stepper({ value, defaultValue = 0, onValueChange }: StepperProps) {
const [count, setCount] = useControllableState({ value, defaultValue, onChange: onValueChange });
return <Button onClick={() => setCount((n) => n + 1)}>{count}</Button>;
}
<Stepper defaultValue={3} />; // o'zi boshqaradi
<Stepper value={qty} onValueChange={setQty} />; // ota boshqaradiTip haqida
defaultValue berilsa, qiymat tipi T. Berilmasa — T | undefined.
Kechiktirish
useDebounce
function useDebounce<T>(value: T, ms?: number): T;Qiymatni kechiktiradi: oxirgi o'zgarishdan ms millisekund o'tgach yangilanadi (standart
300). Har bir harfga so'rov yubormaslik uchun — qidiruv maydonining klassik vazifasi.
Natija yozishni to'xtatganingizdan 400 ms keyin
'use client';
import { useDebounce } from '@akhatmirzo/hooks';
import { Badge, Field, Input } from '@akhatmirzo/ui';
import { useState } from 'react';
const CITIES = [
'Toshkent',
'Samarqand',
'Buxoro',
'Xiva',
'Namangan',
'Andijon',
'Nukus',
'Termiz',
];
export default function HooksDebounce() {
const [query, setQuery] = useState('');
const debounced = useDebounce(query, 400);
const results = CITIES.filter((c) => c.toLowerCase().includes(debounced.trim().toLowerCase()));
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<Field label="Shahar qidirish" description="Natija yozishni to'xtatganingizdan 400 ms keyin">
<Input value={query} onChange={(e) => setQuery(e.target.value)} clearable />
</Field>
<Badge color={query === debounced ? 'success' : 'warning'} dot className="self-start">
{query === debounced ? 'Yangilangan' : 'Kutilmoqda…'}
</Badge>
<div className="flex flex-wrap gap-2">
{results.map((city) => (
<Badge key={city} variant="outline">
{city}
</Badge>
))}
{results.length === 0 && (
<span className="text-sm text-text-muted">Hech narsa topilmadi</span>
)}
</div>
</div>
);
}
const [query, setQuery] = useState('');
const debounced = useDebounce(query, 400);
useEffect(() => {
if (debounced) void api.get('/search', { query: { q: debounced } });
}, [debounced]);useDebouncedCallback
function useDebouncedCallback<Args extends unknown[]>(
fn: (...args: Args) => void,
ms?: number,
): DebouncedFunction<Args>;
interface DebouncedFunction<Args> {
(...args: Args): void;
cancel: () => void;
flush: () => void;
isPending: () => boolean;
}Funksiyani kechiktiradi. Qiymat emas, harakat kechiktirilishi kerak bo'lganda — masalan,
avtomatik saqlash. Har doim eng so'nggi fn chaqiriladi (eski closure muammosi yo'q), unmount'da
kutilayotgan chaqiruv bekor qilinadi. ms — standart 300.
Avtomatik saqlash: yozishni to'xtatgach 800 ms o'tib
'use client';
import { useDebouncedCallback } from '@akhatmirzo/hooks';
import { Button, Field, Textarea } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function HooksDebouncedCallback() {
const [text, setText] = useState('');
const [saved, setSaved] = useState<string[]>([]);
const save = useDebouncedCallback((value: string) => {
setSaved((list) => [`${value.length} belgi saqlandi`, ...list].slice(0, 3));
}, 800);
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<Field label="Izoh" description="Avtomatik saqlash: yozishni to'xtatgach 800 ms o'tib">
<Textarea
value={text}
onChange={(e) => {
setText(e.target.value);
save(e.target.value);
}}
minRows={3}
/>
</Field>
<Button size="sm" variant="outline" className="self-start" onClick={save.flush}>
Hozir saqlash
</Button>
<ul className="text-sm text-text-muted">
{saved.map((line, i) => (
<li key={i}>{line}</li>
))}
</ul>
</div>
);
}
const save = useDebouncedCallback((text: string) => api.patch('/notes/1', { text }), 800);
<Textarea onChange={(e) => save(e.target.value)} onBlur={save.flush} />;| Metod | Nima qiladi |
|---|---|
cancel() | Kutilayotgan chaqiruvni bekor qiladi |
flush() | Kutilayotgan chaqiruvni darhol bajaradi |
isPending() | Kutilayotgan chaqiruv bormi |
Brauzer
useLocalStorage
function useLocalStorage<T>(
key: string,
initial: T | (() => T),
): [value: T, setValue: (value: T | ((prev: T) => T)) => void, remove: () => void];localStorage bilan sinxron holat. Qiymat JSON qilib saqlanadi va sinxronlanadi: shu
sahifadagi bir xil kalitli boshqa hooklar va boshqa brauzer tablari ham yangilanadi. remove
kalitni o'chiradi — qiymat initial ga qaytadi.
Boshqa komponent ko'rayotgan qiymat: 0
Sahifani boshqa tabda oching — qiymat sinxron o'zgaradi
'use client';
import { useLocalStorage } from '@akhatmirzo/hooks';
import { Button } from '@akhatmirzo/ui';
function Mirror() {
const [count] = useLocalStorage('docs-demo:counter', 0);
return (
<p className="text-sm text-text-muted">
Boshqa komponent ko'rayotgan qiymat: <strong className="text-text">{count}</strong>
</p>
);
}
export default function HooksLocalStorage() {
const [count, setCount, remove] = useLocalStorage('docs-demo:counter', 0);
return (
<div className="flex flex-col items-center gap-3">
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setCount((n) => n - 1)}>
−
</Button>
<span className="min-w-12 text-center text-2xl font-semibold text-text tabular-nums">
{count}
</span>
<Button variant="outline" onClick={() => setCount((n) => n + 1)}>
+
</Button>
<Button variant="ghost" onClick={remove}>
O'chirish
</Button>
</div>
<Mirror />
<p className="text-xs text-text-faint">
Sahifani boshqa tabda oching — qiymat sinxron o'zgaradi
</p>
</div>
);
}
const [recent, setRecent, clearRecent] = useLocalStorage<string[]>('recent-searches', []);
const remember = (query: string) =>
setRecent((list) => [query, ...list.filter((q) => q !== query)].slice(0, 5));Xatolar otilmaydi: serverda initial qaytadi, storage taqiqlangan yoki to'la bo'lsa qiymat sahifa
xotirasida saqlanadi (holat ishlayveradi), buzuq JSON — initial.
createStorage bilan aralashtirmang
createStorage (@akhatmirzo/core) qiymatni o'z o'ramida
yozadi. Bitta kalitni ikkalasida ishlatmang.
useMediaQuery
function useMediaQuery(query: string, defaultValue?: boolean): boolean;CSS media so'rov mos keladimi — oyna o'lchami yoki tizim sozlamasi o'zgarganda yangilanadi.
Serverda va matchMedia yo'q joyda defaultValue (standart false) qaytadi.
'use client';
import { useMediaQuery } from '@akhatmirzo/hooks';
import { Badge } from '@akhatmirzo/ui';
export default function HooksMediaQuery() {
const queries = [
{ label: '≥ 640px', on: useMediaQuery('(min-width: 640px)') },
{ label: '≥ 1024px', on: useMediaQuery('(min-width: 1024px)') },
{ label: "Tizim: qorong'i mavzu", on: useMediaQuery('(prefers-color-scheme: dark)') },
{ label: 'Kam animatsiya', on: useMediaQuery('(prefers-reduced-motion: reduce)') },
{ label: 'Sichqoncha (hover)', on: useMediaQuery('(hover: hover)') },
];
return (
<div className="flex flex-wrap justify-center gap-2">
{queries.map(({ label, on }) => (
<Badge key={label} color={on ? 'success' : 'neutral'} variant={on ? 'soft' : 'outline'} dot>
{label}
</Badge>
))}
</div>
);
}
const isMobile = useMediaQuery('(max-width: 639px)');
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
return isMobile ? <Drawer>…</Drawer> : <Dialog>…</Dialog>;useClickOutside
function useClickOutside(
refs: RefObject<Element | null> | ReadonlyArray<RefObject<Element | null>>,
handler: (event: PointerEvent) => void,
enabled?: boolean,
): void;Berilgan element(lar)dan tashqarida pointerdown bo'lganda handler chaqiriladi. Bir
nechta ref berish mumkin — masalan, tugma va panel: tugmaning o'ziga bosish "tashqari" hisoblanmaydi.
enabled (standart true) — panel yopiq paytda tinglamaslik uchun.
'use client';
import { useClickOutside, useHotkeys, useToggle } from '@akhatmirzo/hooks';
import { Button } from '@akhatmirzo/ui';
import { useRef } from 'react';
export default function HooksClickOutside() {
const [open, toggle, setOpen] = useToggle(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
useClickOutside([triggerRef, panelRef], () => setOpen(false), open);
useHotkeys('esc', () => setOpen(false), { enabled: open });
return (
<div className="relative flex flex-col items-center gap-2">
<Button ref={triggerRef} variant="outline" onClick={toggle} aria-expanded={open}>
Filtrlar
</Button>
{open && (
<div
ref={panelRef}
className="w-64 rounded-xl border border-border bg-surface-raised p-4 text-sm text-text shadow-lg"
>
Panel tashqarisiga bosing yoki <kbd className="font-mono">Esc</kbd> ni bosing — yopiladi.
</div>
)}
</div>
);
}
const triggerRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
useClickOutside([triggerRef, panelRef], () => setOpen(false), open);DOM'dan olib tashlangan elementga bosish (masalan, bosilganda yo'qolgan menyu bandi) hisobga olinmaydi.
useCopyToClipboard
function useCopyToClipboard(resetMs?: number): {
copied: boolean;
copy: (text: string) => Promise<boolean>;
};Matnni buferga nusxalaydi. copy muvaffaqiyatni qaytaradi va xato otmaydi. copied —
resetMs davomida (standart 2000) true, keyin o'zi false ga qaytadi; 0 — qaytmaydi.
Clipboard API ishlamasa, eski usulga (execCommand('copy')) o'tadi.
pnpm add @akhatmirzo/hooks'use client';
import { useCopyToClipboard } from '@akhatmirzo/hooks';
import { Button } from '@akhatmirzo/ui';
import { Check, Copy } from 'lucide-react';
export default function HooksCopy() {
const { copied, copy } = useCopyToClipboard();
const command = 'pnpm add @akhatmirzo/hooks';
return (
<div className="flex flex-wrap items-center gap-3">
<code className="rounded-lg border border-border bg-surface-muted px-3 py-2 font-mono text-sm text-text">
{command}
</code>
<Button
variant={copied ? 'soft' : 'outline'}
leftIcon={copied ? <Check /> : <Copy />}
onClick={() => void copy(command)}
>
{copied ? 'Nusxalandi' : 'Nusxa olish'}
</Button>
</div>
);
}
const { copied, copy } = useCopyToClipboard();
<Button onClick={() => copy(promoCode)}>{copied ? 'Nusxalandi' : 'Nusxa olish'}</Button>;useIntersection
function useIntersection<T extends Element = Element>(
options?: IntersectionObserverInit & { enabled?: boolean },
): [ref: (node: T | null) => void, entry: IntersectionObserverEntry | null];Element ko'rinish maydoniga kirganini kuzatadi — cheksiz scroll va kech yuklash uchun. Callback
ref qaytaradi, shuning uchun element keyinroq yoki shartli render bo'lsa ham kuzatiladi.
enabled: false — kuzatish to'xtaydi (masalan, boshqa sahifa qolmaganda). IntersectionObserver
yo'q muhitda entry — null.
- Buyurtma #1000
- Buyurtma #1001
- Buyurtma #1002
- Buyurtma #1003
- Buyurtma #1004
- Buyurtma #1005
- Buyurtma #1006
- Buyurtma #1007
- Buyurtma #1008
- Buyurtma #1009
- Buyurtma #1010
- Buyurtma #1011
'use client';
import { useIntersection } from '@akhatmirzo/hooks';
import { Spinner } from '@akhatmirzo/ui';
import { useEffect, useState } from 'react';
const MAX = 60;
export default function HooksIntersection() {
const [count, setCount] = useState(12);
const done = count >= MAX;
const [sentinelRef, entry] = useIntersection<HTMLDivElement>({
rootMargin: '40px',
enabled: !done,
});
const visible = entry?.isIntersecting ?? false;
useEffect(() => {
if (!visible || done) return;
// Haqiqiy ilovada — keyingi sahifani serverdan yuklash
const timer = setTimeout(() => setCount((n) => Math.min(MAX, n + 12)), 500);
return () => clearTimeout(timer);
}, [visible, done, count]);
return (
<div className="h-64 w-full max-w-sm overflow-auto rounded-xl border border-border bg-surface">
<ul className="divide-y divide-border text-sm text-text">
{Array.from({ length: count }, (_, i) => (
<li key={i} className="px-4 py-2">
Buyurtma #{1000 + i}
</li>
))}
</ul>
<div
ref={sentinelRef}
className="flex items-center justify-center gap-2 p-3 text-sm text-text-muted"
>
{done ? (
'Hammasi yuklandi'
) : (
<>
<Spinner size="sm" /> Yuklanmoqda…
</>
)}
</div>
</div>
);
}
const [sentinelRef, entry] = useIntersection<HTMLDivElement>({
rootMargin: '200px',
enabled: hasMore,
});
useEffect(() => {
if (entry?.isIntersecting) loadMore();
}, [entry, items.length]);
return (
<>
{items.map(renderItem)}
<div ref={sentinelRef} />
</>
);Nega items.length bog'liqlikda
Sentinel ko'rinishda qolsa, entry o'zgarmaydi. Ro'yxat uzunligini bog'liqlikka qo'shsangiz,
yangi sahifa yuklangach yana tekshiriladi.
DOM va hodisalar
DOM bilan ishlovchi hooklar RefObject, elementning o'zi yoki null qabul qiladi (null yoki
bo'sh ref — hech narsa qilinmaydi). Ular window / document ga faqat effekt ichida murojaat
qiladi, shuning uchun serverda xato bermaydi. Siz bergan callback'lar "eng so'nggi qiymat"
ref'ida saqlanadi — har renderda yangi funksiya bersangiz ham qayta obuna bo'linmaydi.
useEventListener
function useEventListener<K extends keyof WindowEventMap>(
type: K,
handler: (event: WindowEventMap[K]) => void,
options?: UseEventListenerOptions,
): void;
function useEventListener(
target: Window | Document | HTMLElement | RefObject<HTMLElement | null> | null | undefined,
type: string, // nishonga mos: WindowEventMap / DocumentEventMap / HTMLElementEventMap
handler: (event: Event) => void,
options?: UseEventListenerOptions,
): void;
interface UseEventListenerOptions extends AddEventListenerOptions {
enabled?: boolean;
}Tiplangan hodisa tinglovchisi: event turi nishon va hodisa nomidan o'zi chiqariladi. target
undefined bo'lsa — window, null bo'lsa — tinglanmaydi. Qisqa shakl
useEventListener('resize', fn) — window uchun va render paytida window ga tegmaydi (SSR
uchun eng qulay). enabled: false — vaqtincha o'chirish; capture, passive, once —
addEventListener ga uzatiladi.
Oyna o'lchamini o'zgartiring yoki istalgan tugmani bosing
'use client';
import { useEventListener } from '@akhatmirzo/hooks';
import { Badge, Kbd } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function HooksEventListener() {
const [width, setWidth] = useState<number | null>(null);
const [lastKey, setLastKey] = useState<string | null>(null);
// Qisqa shakl — window uchun, SSR-xavfsiz
useEventListener('resize', () => setWidth(window.innerWidth));
useEventListener('keydown', (event) => setLastKey(event.key));
return (
<div className="flex flex-col items-center gap-3 text-sm text-text">
<p className="text-text-muted">Oyna o'lchamini o'zgartiring yoki istalgan tugmani bosing</p>
<div className="flex flex-wrap items-center justify-center gap-2">
<Badge variant="outline">Kenglik: {width === null ? '—' : `${width}px`}</Badge>
<span className="flex items-center gap-1">
Oxirgi tugma: {lastKey ? <Kbd>{lastKey === ' ' ? 'Space' : lastKey}</Kbd> : '—'}
</span>
</div>
</div>
);
}
useEventListener('resize', () => setWidth(window.innerWidth));
useEventListener(document, 'visibilitychange', onVisibility);
const buttonRef = useRef<HTMLButtonElement>(null);
useEventListener(buttonRef, 'pointerenter', () => prefetch('/orders'));
useEventListener('scroll', onScroll, { passive: true, enabled: open });useResizeObserver
function useResizeObserver<T extends Element = Element>(
target: RefObject<T | null> | T | null,
options?: { box?: ResizeObserverBoxOptions; enabled?: boolean },
): { width: number; height: number } | null;Element o'lchamini kuzatadi. Bir kadr ichidagi bir nechta o'zgarish requestAnimationFrame bilan
bitta yangilanishga birlashtiriladi, o'lcham o'zgarmasa qayta render bo'lmaydi. box —
'content-box' (standart), 'border-box' yoki 'device-pixel-content-box'. Birinchi
o'lchovgacha, serverda va ResizeObserver yo'q muhitda — null.
'use client';
import { useResizeObserver } from '@akhatmirzo/hooks';
import { Badge } from '@akhatmirzo/ui';
import { useRef } from 'react';
export default function HooksResizeObserver() {
const ref = useRef<HTMLDivElement>(null);
const size = useResizeObserver(ref, { box: 'border-box' });
const compact = size !== null && size.width < 240;
return (
<div
ref={ref}
className="flex h-40 w-72 min-w-40 max-w-full resize flex-col items-center justify-center gap-2 overflow-auto rounded-xl border border-border bg-surface p-4 text-sm text-text"
>
<Badge color={compact ? 'warning' : 'success'} dot>
{compact ? 'Ixcham' : 'Keng'}
</Badge>
<span className="font-mono text-text-muted">
{size ? `${Math.round(size.width)} × ${Math.round(size.height)}` : '…'}
</span>
<span className="text-xs text-text-muted">O'ng pastki burchakdan torting</span>
</div>
);
}
const ref = useRef<HTMLDivElement>(null);
const size = useResizeObserver(ref);
const columns = size && size.width < 480 ? 1 : 3;
return (
<div ref={ref} className="grid" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
…
</div>
);useScrollLock
function useScrollLock(
locked: boolean,
options?: { target?: RefObject<HTMLElement | null> | HTMLElement | null },
): void;locked true bo'lganda sahifa (standart document.body) yoki target scroll'ini qulflaydi —
modal, drawer, mobil menyu uchun. Yo'qolgan scrollbar kengligi padding-right bilan qoplanadi,
shuning uchun kontent sakramaydi. iOS va scroll zanjiri uchun overflow: hidden bilan birga
overscroll-behavior: none qo'yiladi.
'use client';
import { useScrollLock, useToggle } from '@akhatmirzo/hooks';
import { Badge, Button } from '@akhatmirzo/ui';
export default function HooksScrollLock() {
const [locked, toggle] = useToggle(false);
useScrollLock(locked);
return (
<div className="flex flex-col items-center gap-3">
<Button variant={locked ? 'primary' : 'outline'} onClick={toggle} aria-pressed={locked}>
{locked ? 'Scroll qulfini ochish' : "Sahifa scroll'ini qulflash"}
</Button>
<Badge color={locked ? 'warning' : 'neutral'} dot>
{locked ? "Sahifa qulflangan — g'ildirak bilan aylantirib ko'ring" : 'Sahifa erkin'}
</Badge>
</div>
);
}
const [open, setOpen] = useState(false);
useScrollLock(open);Bir nechta qulf
Bir vaqtda bir nechta komponent qulflasa (masalan, drawer ustida dialog), hisoblagich ishlaydi: scroll oxirgi qulf ochilgandagina qaytadi va asl inline uslublar tiklanadi.
useFocusTrap
function useFocusTrap(
target: RefObject<HTMLElement | null> | HTMLElement | null,
active: boolean,
options?: {
initialFocus?: RefObject<HTMLElement | null> | HTMLElement | null;
returnFocus?: boolean;
},
): void;active paytida fokus konteyner ichida qoladi: Tab oxirgi elementdan birinchisiga, Shift+Tab
birinchisidan oxirgisiga aylanadi, tashqariga chiqqan fokus qaytariladi. Faollashganda
initialFocus yoki birinchi fokuslanuvchi element (u ham bo'lmasa — konteynerning o'zi)
fokuslanadi. O'chirilganda yoki unmount'da fokus avvalgi elementga qaytadi (returnFocus,
standart true). Ichma-ich tuzoqlarda faqat oxirgi faollashgani ishlaydi.
'use client';
import { useFocusTrap, useHotkeys, useToggle } from '@akhatmirzo/hooks';
import { Button, Field, Input } from '@akhatmirzo/ui';
import { useRef } from 'react';
export default function HooksFocusTrap() {
const [open, toggle, setOpen] = useToggle(false);
const panelRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useFocusTrap(panelRef, open, { initialFocus: inputRef });
useHotkeys('esc', () => setOpen(false), { enabled: open, enableOnInputs: true });
return (
<div className="flex w-full max-w-sm flex-col items-center gap-3">
<Button variant="outline" onClick={toggle} aria-expanded={open}>
{open ? 'Yopish' : "Manzil qo'shish"}
</Button>
{open && (
<div
ref={panelRef}
className="flex w-full flex-col gap-3 rounded-xl border border-border bg-surface-raised p-4 shadow-lg"
>
<Field label="Shahar">
<Input ref={inputRef} placeholder="Toshkent" />
</Field>
<Field label="Ko'cha">
<Input placeholder="Amir Temur ko'chasi, 15" />
</Field>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={() => setOpen(false)}>
Bekor qilish
</Button>
<Button variant="primary" onClick={() => setOpen(false)}>
Saqlash
</Button>
</div>
</div>
)}
</div>
);
}
const panelRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useFocusTrap(panelRef, open, { initialFocus: inputRef });
useHotkeys('esc', () => setOpen(false), { enabled: open, enableOnInputs: true });Esc va ARIA sizning zimmangizda
Hook faqat fokusni boshqaradi. Yopish tugmasi, Esc, role="dialog" va aria-modal ni o'zingiz
qo'shing yoki tayyor Dialog / Drawer komponentlarini ishlating.
useLongPress
function useLongPress<T extends Element = Element>(
callback: (event: PointerEvent<T>) => void,
options?: {
delay?: number; // 500
onStart?: (event: PointerEvent<T>) => void;
onCancel?: (event: PointerEvent<T>) => void;
moveThreshold?: number; // 10
preventContextMenu?: boolean; // true
enabled?: boolean; // true
},
): LongPressHandlers<T>;Uzoq bosishni aniqlaydi — sichqoncha, barmoq va qalam uchun bir xil (Pointer Events). Elementga
yoyiladigan barqaror ishlovchilar qaytaradi. delay ms o'tgach callback chaqiriladi; undan
oldin qo'yib yuborilsa, element chetidan chiqilsa yoki barmoq moveThreshold px dan ko'p siljisa
(scroll) — bekor qilinadi va onCancel chaqiriladi. Sichqonchaning faqat chap tugmasi hisobga
olinadi. Bosib turilganda brauzer kontekst menyusi bloklanadi (preventContextMenu: false —
bloklamaslik).
'use client';
import { useLongPress } from '@akhatmirzo/hooks';
import { Badge } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function HooksLongPress() {
const [status, setStatus] = useState<'idle' | 'pressing' | 'done' | 'cancelled'>('idle');
const handlers = useLongPress<HTMLButtonElement>(() => setStatus('done'), {
delay: 700,
onStart: () => setStatus('pressing'),
onCancel: () => setStatus('cancelled'),
});
const label = {
idle: 'Kutilmoqda',
pressing: 'Bosib turibsiz…',
done: "Buyurtma o'chirishga tanlandi",
cancelled: 'Bekor qilindi',
}[status];
return (
<div className="flex flex-col items-center gap-3">
<button
type="button"
{...handlers}
className="w-64 touch-none select-none rounded-xl border border-border bg-surface px-4 py-3 text-left text-sm text-text transition-colors active:bg-surface-muted"
>
<span className="font-medium">Buyurtma #1024</span>
<span className="block text-text-muted">1 250 000 so'm · 0,7 s bosib turing</span>
</button>
<Badge
color={status === 'done' ? 'success' : status === 'cancelled' ? 'warning' : 'neutral'}
dot
>
{label}
</Badge>
</div>
);
}
const handlers = useLongPress<HTMLLIElement>(() => setSelectMode(true), { delay: 600 });
return <li {...handlers}>Buyurtma #1024</li>;Sensorli ekranda matn tanlanib bosishni buzmasligi uchun elementga user-select: none
(select-none) bering. Ro'yxat elementlarida touch-action ni o'zgartirmang: sahifa scroll
qilina boshlasa brauzer pointercancel yuboradi va bosish bekor bo'ladi — scroll'li ro'yxatda
aynan shu kerak. touch-action: none (touch-none) ni faqat sahifani scroll qilishi kerak
bo'lmagan elementlarga qo'shing.
useDocumentVisibility
function useDocumentVisibility(): 'visible' | 'hidden';Sahifa (tab) ko'rinib turibdimi. Foydalanuvchi boshqa tabga o'tganda polling, video yoki
animatsiyani to'xtatish uchun. Serverda — 'visible'.
Faol vaqt: 0 s
Boshqa tabga o'tib qayting — hisob to'xtab turadi
'use client';
import { useDocumentVisibility } from '@akhatmirzo/hooks';
import { Badge } from '@akhatmirzo/ui';
import { useEffect, useState } from 'react';
export default function HooksDocumentVisibility() {
const visibility = useDocumentVisibility();
const [seconds, setSeconds] = useState(0);
// Tab yashirin bo'lsa, taymer to'xtaydi — resurs tejaladi
useEffect(() => {
if (visibility === 'hidden') return;
const id = setInterval(() => setSeconds((s) => s + 1), 1000);
return () => clearInterval(id);
}, [visibility]);
return (
<div className="flex flex-col items-center gap-3 text-sm text-text">
<Badge color={visibility === 'visible' ? 'success' : 'neutral'} dot>
{visibility === 'visible' ? "Tab ko'rinib turibdi" : 'Tab yashirin'}
</Badge>
<p>
Faol vaqt: <span className="font-mono">{seconds} s</span>
</p>
<p className="text-text-muted">Boshqa tabga o'tib qayting — hisob to'xtab turadi</p>
</div>
);
}
const visibility = useDocumentVisibility();
useEffect(() => {
if (visibility === 'hidden') return;
const id = setInterval(refreshOrders, 10_000);
return () => clearInterval(id);
}, [visibility]);useIsClient
function useIsClient(): boolean;Serverda va gidratatsiya paytida false, gidratatsiyadan keyin — true. Faqat brauzerda
ishlaydigan narsani (grafik, window o'lchami, sana "hozir") gidratatsiya xatosisiz ko'rsatish
uchun. useSyncExternalStore ustida qurilgan — keyingi mount'larda darhol true.
const isClient = useIsClient();
return isClient ? <SalesChart /> : <Skeleton className="h-64" />;Ma'lumotlar
useAsync
function useAsync<T, Args extends unknown[] = []>(
fn: (...args: Args) => Promise<T>,
deps?: DependencyList,
options?: { immediate?: boolean },
): {
status: 'idle' | 'loading' | 'success' | 'error';
data: T | undefined;
error: unknown;
run: (...args: Args) => Promise<T | undefined>;
};Asinxron funksiya holati. immediate: true (standart) — mount'da va deps o'zgarganda o'zi
ishga tushadi. immediate: false — faqat run() bilan. Faqat eng so'nggi chaqiruv natijasi
qabul qilinadi: tez-tez bosilganda eski javob yangisining ustiga yozilmaydi. Unmount'dan keyin
kelgan javob e'tiborsiz qoladi.
'use client';
import { useAsync, useToggle } from '@akhatmirzo/hooks';
import { Badge, Button, Switch } from '@akhatmirzo/ui';
const CITIES = ['Toshkent', 'Samarqand', 'Buxoro', 'Xiva', 'Namangan', 'Andijon', 'Nukus'];
const STATUS_COLOR = {
idle: 'neutral',
loading: 'info',
success: 'success',
error: 'danger',
} as const;
function fetchCities(fail: boolean): Promise<string[]> {
const delay = 500 + Math.random() * 1000;
return new Promise((resolve, reject) =>
setTimeout(() => {
if (fail) reject(new Error('Server javob bermadi'));
else resolve(CITIES.slice(0, 2 + Math.floor(Math.random() * 5)));
}, delay),
);
}
export default function HooksAsync() {
const [fail, , setFail] = useToggle(false);
const { status, data, error, run } = useAsync(fetchCities, [], { immediate: false });
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<div className="flex flex-wrap items-center gap-3">
<Button onClick={() => void run(fail)} loading={status === 'loading'}>
Yuklash
</Button>
<Badge color={STATUS_COLOR[status]} dot>
{status}
</Badge>
</div>
<Switch checked={fail} onCheckedChange={setFail} label="Xato bilan tugasin" />
{status === 'error' && error instanceof Error && (
<p className="text-sm text-danger-soft-foreground">{error.message}</p>
)}
{data && <p className="text-sm text-text">{data.join(', ')}</p>}
</div>
);
}
// Sahifa o'zgarganda o'zi yuklaydi
const { status, data, error } = useAsync(
() => api.get<Order[]>('/orders', { query: { page } }),
[page],
);
// Faqat tugma bosilganda
const { run, status: saving } = useAsync((form: OrderForm) => api.post('/orders', form), [], {
immediate: false,
});data— oxirgi muvaffaqiyatli natija. Qayta yuklash va xato paytida ham saqlanadi — ro'yxat yuklanish paytida bo'shab qolmaydi.runeskirgan yoki xato chaqiruvdaundefinedqaytaradi va xato otmaydi; xatoerrorda.
usePagination
function usePagination(params: {
total: number;
pageSize: number;
page: number;
siblings?: number;
}): {
pages: (number | 'ellipsis')[];
pageCount: number;
page: number;
hasPrev: boolean;
hasNext: boolean;
from: number;
to: number;
};Sahifalash tugmalari ro'yxati: 1 … 4 5 6 … 20. Tugmalar soni barqaror (siblings * 2 + 5),
shuning uchun sahifa almashganda panel "sakramaydi". Ko'p nuqta faqat 2 va undan ko'p sahifani
yashirganda qo'yiladi.
41–50 / 200 ta yozuv
'use client';
import { usePagination } from '@akhatmirzo/hooks';
import { Button } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function HooksPagination() {
const [page, setPage] = useState(5);
const { pages, hasPrev, hasNext, from, to } = usePagination({ total: 200, pageSize: 10, page });
return (
<div className="flex flex-col items-center gap-3">
<nav aria-label="Sahifalar" className="flex flex-wrap items-center justify-center gap-1">
<Button size="sm" variant="ghost" disabled={!hasPrev} onClick={() => setPage(page - 1)}>
Oldingi
</Button>
{pages.map((p, i) =>
p === 'ellipsis' ? (
<span key={`e${i}`} className="px-2 text-text-muted">
…
</span>
) : (
<Button
key={p}
size="sm"
variant={p === page ? 'primary' : 'ghost'}
aria-current={p === page ? 'page' : undefined}
onClick={() => setPage(p)}
>
{p}
</Button>
),
)}
<Button size="sm" variant="ghost" disabled={!hasNext} onClick={() => setPage(page + 1)}>
Keyingi
</Button>
</nav>
<p className="text-sm text-text-muted">
{from}–{to} / 200 ta yozuv
</p>
</div>
);
}
usePagination({ total: 200, pageSize: 10, page: 5 });
// { pages: [1, 'ellipsis', 4, 5, 6, 'ellipsis', 20], pageCount: 20, page: 5,
// hasPrev: true, hasNext: true, from: 41, to: 50 }
usePagination({ total: 200, pageSize: 10, page: 1 }).pages; // [1, 2, 3, 4, 5, 'ellipsis', 20]
usePagination({ total: 50, pageSize: 10, page: 2 }).pages; // [1, 2, 3, 4, 5]
usePagination({ total: 95, pageSize: 10, page: 99 }).page; // 10 — chegaraga qisildi| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
totalmajburiy | number | — | Jami yozuvlar soni |
pageSizemajburiy | number | — | Bir sahifadagi yozuvlar |
pagemajburiy | number | — | Joriy sahifa, 1 dan. Chegaradan chiqsa qisiladi |
siblings | number | 1 | Joriy sahifaning har ikki tomonidagi sahifalar soni |
from / to — joriy sahifadagi birinchi va oxirgi yozuv tartib raqami ("41–50 / 200"). Bo'sh
ro'yxatda from — 0. Hook'siz hisoblash kerak bo'lsa (masalan, serverda) — xuddi shu
parametrli sof funksiya getPagination bor.
Klaviatura
useHotkeys
function useHotkeys(
keys: string,
handler: (event: KeyboardEvent) => void,
options?: { enabled?: boolean; preventDefault?: boolean; enableOnInputs?: boolean },
): void;Global klaviatura yorliqlari. mod — macOS'da ⌘, boshqa tizimlarda Ctrl. Bir nechta
kombinatsiya vergul bilan: 'mod+k, /'. Standart holatda input, textarea, select va
contenteditable ichida ishlamaydi — foydalanuvchi yozayotgan matnga xalaqit bermaslik uchun.
Yuqoridagi tugmalardan birini bosing
'use client';
import { useHotkeys } from '@akhatmirzo/hooks';
import { Badge, Kbd } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function HooksHotkeys() {
const [last, setLast] = useState<string | null>(null);
const [count, setCount] = useState(0);
const hit = (name: string) => {
setLast(name);
setCount((n) => n + 1);
};
useHotkeys('alt+k', () => hit('alt+k'));
useHotkeys('?', () => hit('?'));
// Bir nechta kombinatsiya — vergul bilan
useHotkeys('g, alt+g', () => hit('g'));
return (
<div className="flex flex-col items-center gap-3 text-sm text-text">
<div className="flex flex-wrap items-center justify-center gap-3">
<Kbd keys={['Alt', 'K']} />
<Kbd>?</Kbd>
<Kbd>G</Kbd>
</div>
<p className="text-text-muted">Yuqoridagi tugmalardan birini bosing</p>
{last && (
<Badge color="primary">
{last} · {count} marta
</Badge>
)}
</div>
);
}
useHotkeys('mod+k', () => setPaletteOpen(true));
useHotkeys('mod+s', () => save()); // brauzerning "Saqlash" oynasi ochilmaydi
useHotkeys('esc', close, { enabled: open, enableOnInputs: true });| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
enabled | boolean | true | false — tinglamaydi (masalan, panel yopiq bo'lsa) |
preventDefault | boolean | true | Mos kelganda brauzerning standart amalini to'xtatadi |
enableOnInputs | boolean | false | Input, textarea, select va contenteditable ichida ham ishlaydi |
Kombinatsiya yozish:
- Modifikatorlar:
mod,ctrl,meta(cmd),alt(option),shift. - Tugma nomlari: harf va raqamlar,
esc,enter,space,up/down/left/right,del,comma,plusvaKeyboardEvent.keyning boshqa qiymatlari (tab,f1...). - Belgilar:
'?'yoki'/'kabi belgilar Shift bilan yozilsa ham ishlaydi —shiftyozish shart emas. - macOS'da
altharfni o'zgartiradi (Alt+K→˚), lekinalt+kbaribir ishlaydi — fizik tugma tekshiriladi.
Yorliqni tugma yonida ko'rsatish uchun Kbd dan foydalaning:
<Kbd keys={['mod', 'K']} />.
matchHotkey va isApplePlatform
function matchHotkey(event: KeyboardEvent, keys: string): boolean;
function isApplePlatform(): boolean; // serverda falseuseHotkeys ichidagi tekshiruv alohida: o'z onKeyDown ingizda yorliqni tekshirish uchun.
<Input
onKeyDown={(e) => {
if (matchHotkey(e.nativeEvent, 'mod+enter')) submit();
}}
/>Vaqt va jarayon
useInterval
function useInterval(callback: () => void, delay: number | null): void;callback ni har delay millisekundda chaqiradi. delay — null bo'lsa interval to'xtaydi,
qaytib raqam bo'lsa — yana boshlanadi. Callback har renderda yangi bo'lsa ham interval qayta
boshlanmaydi: har safar eng so'nggi funksiya chaqiriladi, shuning uchun ichida joriy holatni
bemalol o'qish mumkin.
'use client';
import { useInterval } from '@akhatmirzo/hooks';
import { Button } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function HooksInterval() {
const [seconds, setSeconds] = useState(0);
const [running, setRunning] = useState(true);
// null — interval to'xtaydi
useInterval(() => setSeconds((s) => s + 1), running ? 1000 : null);
const mm = String(Math.floor(seconds / 60)).padStart(2, '0');
const ss = String(seconds % 60).padStart(2, '0');
return (
<div className="flex flex-col items-center gap-4">
<span className="text-3xl font-semibold tabular-nums text-text">
{mm}:{ss}
</span>
<div className="flex gap-2">
<Button variant="primary" onClick={() => setRunning((r) => !r)}>
{running ? "To'xtatish" : 'Davom ettirish'}
</Button>
<Button variant="ghost" onClick={() => setSeconds(0)}>
Nolga qaytarish
</Button>
</div>
</div>
);
}
const [seconds, setSeconds] = useState(0);
const [running, setRunning] = useState(true);
useInterval(() => setSeconds((s) => s + 1), running ? 1000 : null);useTimeout
function useTimeout(
callback: () => void,
delay: number | null,
): { reset: () => void; clear: () => void };callback ni delay millisekunddan keyin bir marta chaqiradi. Taymer mount'da va delay
o'zgarganda boshlanadi, unmount'da bekor qilinadi. clear() — kutilayotgan chaqiruvni bekor
qiladi, reset() — taymerni hozirdan boshlab qaytadan boshlaydi. delay: null — taymer
qo'yilmaydi.
// Xabar 4 soniyadan keyin yopiladi; sichqoncha ustida turganda — yopilmaydi
const { reset, clear } = useTimeout(() => setVisible(false), 4000);
<div onMouseEnter={clear} onMouseLeave={reset}>
Saqlandi
</div>;useCountdown
function useCountdown(
seconds: number,
options?: { autoStart?: boolean; onEnd?: () => void },
): {
remaining: number;
isRunning: boolean;
start: () => void;
pause: () => void;
reset: () => void;
restart: () => void;
formatted: string; // "0:59"
};Teskari sanoq — SMS kodni qayta yuborish, taklif muddati, test vaqti uchun. Vaqt Date.now()
farqi bilan hisoblanadi: brauzer fon tabdagi taymerlarni sekinlashtirsa ham, foydalanuvchi
qaytganda qolgan vaqt to'g'ri ko'rinadi. formatted — "m:ss" ko'rinishida ("2:05",
"0:59").
+998 90 *** ** 67 ga yuborildi
'use client';
import { useCountdown } from '@akhatmirzo/hooks';
import { Button, Field, OtpInput, toast } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function HooksCountdown() {
const [code, setCode] = useState('');
const { formatted, isRunning, restart } = useCountdown(59);
const resend = () => {
// Haqiqiy ilovada — SMS yuborish so'rovi
setCode('');
toast('Yangi kod +998 90 *** ** 67 ga yuborildi');
restart();
};
return (
<div className="flex flex-col items-start gap-3">
<Field label="Tasdiqlash kodi" description="+998 90 *** ** 67 ga yuborildi">
<OtpInput groupSize={3} value={code} onValueChange={setCode} />
</Field>
<Button variant="ghost" size="sm" disabled={isRunning} onClick={resend}>
{isRunning ? `Kodni qayta yuborish (${formatted})` : 'Kodni qayta yuborish'}
</Button>
</div>
);
}
const { formatted, isRunning, restart } = useCountdown(59);
<Button
disabled={isRunning}
onClick={() => {
sendSms();
restart();
}}
>
{isRunning ? `Kodni qayta yuborish (${formatted})` : 'Kodni qayta yuborish'}
</Button>;start()— pauzadan davom ettiradi. Sanoq tugagan bo'lsa (remaining: 0),secondsdan qaytadan boshlaydi.pause()— to'xtatib turadi, qolgan vaqt saqlanadi.reset()—secondsga qaytaradi va to'xtatadi;restart()— qaytaradi va darhol boshlaydi.onEnd— sanoq 0 ga yetganda har bir yugurishda bir marta chaqiriladi.secondso'zgarsa joriy sanoqqa ta'sir qilmaydi — keyingireset/restartda qo'llanadi.
| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
autoStart | boolean | true | Mount'da o'zi boshlanadi |
onEnd | () => void | — | Sanoq 0 ga yetganda |
OtpInput.ResendTimer
Faqat "qayta yuborish" tugmasi kerak bo'lsa — tayyor
OtpInput.ResendTimer bor. useCountdown — o'z ko'rinishingiz
kerak bo'lganda.
useIdle
function useIdle(ms: number, options?: { events?: string[]; initialState?: boolean }): boolean;Foydalanuvchi ms millisekund davomida hech narsa qilmasa — true. Har bir faollik (sichqoncha
harakati, tugma bosish, teginish, scroll, tabga qaytish) taymerni qaytadan boshlaydi va holatni
false ga qaytaradi. Avtomatik chiqish yoki "Hali shu yerdamisiz?" oynasi uchun.
5 soniya sichqoncha va klaviaturaga tegmang. Tarmoqni o'chirib ko'ring — holat darhol yangilanadi.
'use client';
import { useIdle, useOnline } from '@akhatmirzo/hooks';
import { Badge } from '@akhatmirzo/ui';
export default function HooksIdle() {
const idle = useIdle(5000);
const online = useOnline();
return (
<div className="flex flex-col items-center gap-3 text-center">
<div className="flex flex-wrap justify-center gap-2">
<Badge color={idle ? 'warning' : 'success'} dot>
{idle ? 'Nofaol' : 'Faol'}
</Badge>
<Badge color={online ? 'success' : 'danger'} dot>
{online ? 'Internet bor' : "Internet yo'q"}
</Badge>
</div>
<p className="max-w-xs text-sm text-text-muted">
5 soniya sichqoncha va klaviaturaga tegmang. Tarmoqni o'chirib ko'ring — holat darhol
yangilanadi.
</p>
</div>
);
}
const idle = useIdle(15 * 60_000);
useEffect(() => {
if (idle) logout();
}, [idle]);| Prop | Tip | Standart | Tavsif |
|---|---|---|---|
events | string[] | ['mousemove', 'keydown', 'touchstart', 'scroll', 'visibilitychange'] | Faollik hodisalari. visibilitychange — document'da, qolgani window'da |
initialState | boolean | false | Mount'dagi holat |
useStep
function useStep(
count: number,
options?: { initial?: number },
): {
step: number;
next: () => void;
prev: () => void;
goTo: (step: number) => void;
reset: () => void;
isFirst: boolean;
isLast: boolean;
progress: number; // 0..1
};Ko'p bosqichli forma (wizard) holati. Qadam 0 dan boshlanadi va doim 0..count - 1 oralig'ida:
next() oxirgi qadamda, prev() birinchisida hech narsa qilmaydi, goTo() chegaraga qisiladi.
progress — birinchi qadamda 0, oxirgisida 1. Stepper bilan
juft ishlaydi.
- 1-qadam, 4 tadan: Savat
- 2-qadam, 4 tadan: Yetkazib berishManzil va vaqt
- 3-qadam, 4 tadan: To'lov
- 4-qadam, 4 tadan: Tasdiqlash
1-qadam, 4 tadan:Savat
'use client';
import { useStep } from '@akhatmirzo/hooks';
import { Button, Progress, Stepper, toast } from '@akhatmirzo/ui';
const steps = [
{ title: 'Savat' },
{ title: 'Yetkazib berish', description: 'Manzil va vaqt' },
{ title: "To'lov" },
{ title: 'Tasdiqlash' },
];
export default function HooksStep() {
const { step, next, prev, goTo, reset, isFirst, isLast, progress } = useStep(steps.length);
return (
<div className="flex w-full flex-col gap-6">
<Stepper steps={steps} value={step} onValueChange={goTo} aria-label="Buyurtma" />
<Progress value={progress * 100} aria-label="Jarayon" />
<div className="flex gap-2">
<Button size="sm" disabled={isFirst} onClick={prev}>
Orqaga
</Button>
<Button
size="sm"
variant="primary"
onClick={() => {
if (!isLast) return next();
toast('Buyurtma qabul qilindi');
reset();
}}
>
{isLast ? 'Buyurtma berish' : 'Davom etish'}
</Button>
</div>
</div>
);
}
const { step, next, prev, goTo, isFirst, isLast } = useStep(steps.length);
<Stepper steps={steps} value={step} onValueChange={goTo} />
<Button disabled={isFirst} onClick={prev}>Orqaga</Button>
<Button onClick={isLast ? submit : next}>{isLast ? 'Buyurtma berish' : 'Davom etish'}</Button>useInfiniteScroll
function useInfiniteScroll<T extends Element = HTMLDivElement>(options: {
hasMore: boolean;
loading: boolean;
onLoadMore: () => void;
rootMargin?: string; // '200px'
}): { sentinelRef: (node: T | null) => void };Cheksiz scroll — useIntersection ustida tayyor yechim. Ro'yxat oxiriga
sentinel element qo'ying: u ko'rinishga rootMargin qadar yaqinlashganda onLoadMore
chaqiriladi. loading: true paytida qayta chaqirilmaydi, hasMore: false bo'lsa kuzatish
to'xtaydi. Yuklash tugagach sentinel hali ham ko'rinib tursa (sahifa qisqa bo'lsa), keyingi
sahifa o'zi yuklanadi — ekran to'lguncha.
- Buyurtma #1000
- Buyurtma #1001
- Buyurtma #1002
- Buyurtma #1003
- Buyurtma #1004
- Buyurtma #1005
- Buyurtma #1006
- Buyurtma #1007
- Buyurtma #1008
- Buyurtma #1009
- Buyurtma #1010
- Buyurtma #1011
'use client';
import { useInfiniteScroll } from '@akhatmirzo/hooks';
import { Spinner } from '@akhatmirzo/ui';
import { useState } from 'react';
const TOTAL = 60;
const PAGE = 12;
export default function HooksInfiniteScroll() {
const [items, setItems] = useState(PAGE);
const [loading, setLoading] = useState(false);
const hasMore = items < TOTAL;
const { sentinelRef } = useInfiniteScroll({
hasMore,
loading,
rootMargin: '40px',
onLoadMore: () => {
// Haqiqiy ilovada — keyingi sahifani serverdan yuklash
setLoading(true);
setTimeout(() => {
setItems((n) => Math.min(TOTAL, n + PAGE));
setLoading(false);
}, 600);
},
});
return (
<div className="h-64 w-full max-w-sm overflow-auto rounded-xl border border-border bg-surface">
<ul className="divide-y divide-border text-sm text-text">
{Array.from({ length: items }, (_, i) => (
<li key={i} className="px-4 py-2">
Buyurtma #{1000 + i}
</li>
))}
</ul>
<div
ref={sentinelRef}
className="flex items-center justify-center gap-2 p-3 text-sm text-text-muted"
>
{hasMore ? (
<>
<Spinner size="sm" /> Yuklanmoqda…
</>
) : (
'Hammasi yuklandi'
)}
</div>
</div>
);
}
const { data, loading, hasMore, loadNextPage } = useOrders();
const { sentinelRef } = useInfiniteScroll({ hasMore, loading, onLoadMore: loadNextPage });
return (
<>
{data.map((order) => (
<OrderRow key={order.id} order={order} />
))}
<div ref={sentinelRef}>{hasMore && <Spinner size="sm" />}</div>
</>
);usePrevious
function usePrevious<T>(value: T): T | undefined;Qiymatning oldingi holati; birinchi renderda — undefined. Faqat qiymat o'zgarganda
yangilanadi: boshqa sabab bilan qayta render bo'lsa ham oldingi qiymat saqlanadi
(taqqoslash — Object.is).
const prevPage = usePrevious(page);
const direction = prevPage !== undefined && page < prevPage ? 'orqaga' : 'oldinga';useOnline
function useOnline(): boolean;Brauzer tarmoqqa ulanganmi — navigator.onLine va online / offline hodisalari. Serverda va
gidratatsiya paytida true qaytaradi, shuning uchun "Internet yo'q" ogohlantirishi sahifa
yuklanganda bir lahzaga chiqib qolmaydi. Jonli namunasi — useIdle demosida.
function OfflineBanner() {
const online = useOnline();
if (online) return null;
return (
<Alert banner color="warning">
Internet aloqasi yo'q. O'zgarishlar saqlanmaydi.
</Alert>
);
}