Hooklar
@akhatmirzo/hooks — 12 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) |
useAsync | Asinxron funksiya holati |
usePagination | Sahifalash tugmalari |
useHotkeys | Klaviatura yorliqlari |
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.
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();
}}
/>