Timing
import { debounce, memo, throttle, wait } from '@studiometa/js-toolkit/utils';Rate limiting
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 delayNot 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
debounce<Args>(fn: (...args: Args) => void, delay?: number): (...args: Args) => voidRuns once, after the calls stop.
throttle
throttle<Args>(fn: (...args: Args) => void, delay?: number): (...args: Args) => voidRuns at most once per delay.
Waiting
wait
wait(delay?: number): Promise<void>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
memo<Args extends [] | [key: unknown], Value>(fn: (...args: Args) => Value): Memo<Args, Value>import { memo } from '@studiometa/js-toolkit/utils';
const expensive = memo((key) => key.toString().repeat(2));
expensive('a'); // computed
expensive('a'); // cachedZero 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.
import { noop, noopValue } from '@studiometa/js-toolkit/utils';
const onDone = noop; // a callback that does nothing
const identity = noopValue; // a transform that changes nothingnoop
noop(): voidnoopValue
noopValue<T>(value: T): TWhat is not here
| Not shipped | Write |
|---|---|
nextTick | await Promise.resolve() |
nextMicrotask | queueMicrotask(fn) |
Queue, SmartQueue | the scheduler's lanes |
domScheduler, useScheduler | defaultScheduler |