Utilities
debounce, throttle, sleep, groupBy, uniqueBy and clamp — general-purpose helpers.
Small functions you end up rewriting in every project. No dependencies, no React, fully typed.
import { debounce, throttle, sleep, groupBy, uniqueBy, clamp } from '@akhatmirzo/core';Rate limiting
Move the pointer over the area and compare the counters: throttle fires once every 400 ms while
you move, debounce fires once after you stop.
- raw
- 0
- throttled
- 0
- debounced
- 0
'use client';
import { debounce, throttle } from '@akhatmirzo/core';
import { Button } from '@akhatmirzo/ui';
import { useEffect, useMemo, useState } from 'react';
type Counts = { raw: number; throttled: number; debounced: number };
const ZERO: Counts = { raw: 0, throttled: 0, debounced: 0 };
const inc = (key: keyof Counts) => (c: Counts) => ({ ...c, [key]: c[key] + 1 });
export default function CoreDebounceThrottle() {
const [counts, setCounts] = useState(ZERO);
const onDebounced = useMemo(() => debounce(() => setCounts(inc('debounced')), 400), []);
const onThrottled = useMemo(() => throttle(() => setCounts(inc('throttled')), 400), []);
useEffect(
() => () => {
onDebounced.cancel();
onThrottled.cancel();
},
[onDebounced, onThrottled],
);
return (
<div className="flex w-full max-w-md flex-col gap-3">
<div
onPointerMove={() => {
setCounts(inc('raw'));
onDebounced();
onThrottled();
}}
className="flex h-32 touch-none items-center justify-center rounded-xl border border-dashed border-border-strong bg-surface-muted text-sm text-text-muted"
>
Sichqoncha yoki barmoqni shu yerda yurgizing
</div>
<dl className="grid grid-cols-3 gap-2 text-center text-sm">
{(['raw', 'throttled', 'debounced'] as const).map((key) => (
<div key={key} className="rounded-lg border border-border bg-surface p-2">
<dt className="font-mono text-xs text-text-muted">{key}</dt>
<dd className="text-xl font-semibold text-text tabular-nums">{counts[key]}</dd>
</div>
))}
</dl>
<Button size="sm" variant="ghost" onClick={() => setCounts(ZERO)}>
Nolga qaytarish
</Button>
</div>
);
}
debounce
function debounce<A extends unknown[]>(fn: (...args: A) => unknown, ms: number): Debounced<A>;
interface Debounced<A> {
(...args: A): void;
cancel(): void; // drops the pending call
flush(): void; // runs the pending call right away
pending(): boolean; // whether a call is pending
}Calls fn once, ms milliseconds after the last call, with the latest arguments. Use it for
search fields, autosave and window resizing.
const search = debounce((query: string) => api.get('/search', { query: { q: query } }), 300);
search('t');
search('to');
search('tosh'); // after 300 ms, a single request: q=tosh
search.pending(); // true
search.flush(); // send now (e.g. when Enter is pressed)
search.cancel(); // drop it (e.g. when the component closes)Inside a React component
Don't create a new debounce on every render.
useDebouncedCallback handles that for you and cancels on
unmount.
throttle
function throttle<A extends unknown[]>(fn: (...args: A) => unknown, ms: number): Throttled<A>;
interface Throttled<A> {
(...args: A): void;
cancel(): void; // drops the pending (trailing) call
}Calls fn at most once every ms: the first call runs immediately, and the last call made
during the interval runs when the interval ends. Use it for scroll, pointer movement and other
high-frequency events.
const onScroll = throttle(() => saveScrollPosition(window.scrollY), 100);
window.addEventListener('scroll', onScroll);
const log = throttle((n: number) => console.log(n), 50);
log(1); // immediately: 1
log(2);
log(3); // after 50 ms: 3 (2 is skipped)sleep
function sleep(ms: number, signal?: AbortSignal): Promise<void>;Waits ms milliseconds. If signal is aborted, the promise rejects with signal.reason (or an
AbortError), so the wait never hangs.
await sleep(500);
// Try 3 times, waiting 1 and then 2 seconds in between
async function getStatus() {
for (let attempt = 0; ; attempt++) {
try {
return await api.get('/status');
} catch (error) {
if (attempt === 2) throw error;
await sleep(1000 * 2 ** attempt);
}
}
}
const controller = new AbortController();
sleep(10_000, controller.signal).catch((e) => e.name); // 'AbortError'
controller.abort();Arrays
groupBy
function groupBy<T, K extends PropertyKey>(
items: readonly T[],
keyFn: (item: T, index: number) => K,
): Record<K, T[]>;Groups items by key and keeps their order. The result has no prototype, so keys like
'constructor' or 'toString' are safe.
const users = [
{ name: 'Ali', role: 'admin' },
{ name: 'Vali', role: 'user' },
{ name: 'Sardor', role: 'admin' },
];
groupBy(users, (u) => u.role);
// {
// admin: [{ name: 'Ali', … }, { name: 'Sardor', … }],
// user: [{ name: 'Vali', … }],
// }uniqueBy
function uniqueBy<T>(items: readonly T[], keyFn: (item: T, index: number) => unknown): T[];Removes duplicates by key — the first item for each key wins.
uniqueBy(
[
{ id: 1, name: 'Toshkent' },
{ id: 2, name: 'Samarqand' },
{ id: 1, name: 'Toshkent (eski)' },
],
(city) => city.id,
);
// [{ id: 1, name: 'Toshkent' }, { id: 2, name: 'Samarqand' }]Numbers
clamp
function clamp(value: number, min: number, max: number): number;Keeps a value within [min, max].
clamp(150, 0, 100); // 100
clamp(-5, 0, 100); // 0
clamp(42, 0, 100); // 42