Skip to content

Timing

js
import { 
debounce
,
memo
,
throttle
,
wait
} from '@studiometa/js-toolkit/utils';

Rate limiting

js
import { 
debounce
,
throttle
} from '@studiometa/js-toolkit/utils';
const
search
=
debounce
((
term
) =>
console
.
log
(
term
), 300); // after the last call
const
track
=
throttle
((
y
) =>
console
.
log
(
y
), 100); // at most once per delay

Not for scroll, resize, pointer or frame work

The services already coalesce: the scroll service batches its events into one read per frame, and the resize service is a ResizeObserver. A throttle on top of that is a second, worse rate limiter.

Reach for these for what the framework does not own — a fetch per keystroke, an analytics call, a localStorage write.

debounce

ts
debounce<Args>(fn: (...args: Args) => void, delay?: number): (...args: Args) => void

Runs once, after the calls stop.

throttle

ts
throttle<Args>(fn: (...args: Args) => void, delay?: number): (...args: Args) => void

Runs at most once per delay.

Waiting

wait

ts
wait(delay?: number): Promise<void>
js
import { 
wait
} from '@studiometa/js-toolkit/utils';
async function
pause
() {
await
wait
(300);
}

await wait() with no argument is one turn of the event loop.

For a frame rather than a timer, use nextFrame(). For "the framework has caught up", use whenDOMSettled() — a timer is the wrong tool for both, and the reason the test helpers exist at all.

Memoisation

memo

ts
memo<Args extends [] | [key: unknown], Value>(fn: (...args: Args) => Value): Memo<Args, Value>
js
import { 
memo
} from '@studiometa/js-toolkit/utils';
const
expensive
=
memo
((
key
) =>
key
.toString().repeat(2));
expensive
('a'); // computed
expensive
('a'); // cached

Zero or one argument, and that argument is the key. That is the whole surface, and it is deliberate: a memo keyed on several arguments needs a key strategy, and a key strategy is a decision the caller should make visibly rather than inherit.

It is what the four memoised string converters are built on, and what memoises the active breakpoint name for the length of one task.

cache and memoize from v3 are not shipped — this covers the one case core needed.

Placeholders

They earn their place as defaults: a parameter defaulting to noop removes an if from every call site, and one defaulting to noopValue removes it from a transform pipeline. Both are one shared function, so a default costs no allocation per call.

js
import { 
noop
,
noopValue
} from '@studiometa/js-toolkit/utils';
const
onDone
=
noop
; // a callback that does nothing
const
identity
=
noopValue
; // a transform that changes nothing

noop

ts
noop(): void

noopValue

ts
noopValue<T>(value: T): T

What is not here

Not shippedWrite
nextTickawait Promise.resolve()
nextMicrotaskqueueMicrotask(fn)
Queue, SmartQueuethe scheduler's lanes
domScheduler, useSchedulerdefaultScheduler

MIT Licensed