Timing
import { debounce, memo, throttle, wait } from '@studiometa/js-toolkit-v4/utils';debounce and throttle
debounce<Args>(fn: (...args: Args) => void, delay?: number): (...args: Args) => void
throttle<Args>(fn: (...args: Args) => void, delay?: number): (...args: Args) => voidimport { debounce, throttle } from '@studiometa/js-toolkit-v4/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| Function | Runs |
|---|---|
debounce | once, after the calls stop |
throttle | 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.
wait
wait(delay?: number): Promise<void>import { wait } from '@studiometa/js-toolkit-v4/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.
memo
memo<Args extends [] | [key: unknown], Value>(fn: (...args: Args) => Value): Memo<Args, Value>import { memo } from '@studiometa/js-toolkit-v4/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.
noop and noopValue
noop(): void
noopValue<T>(value: T): Timport { noop, noopValue } from '@studiometa/js-toolkit-v4/utils';
const onDone = noop; // a callback that does nothing
const identity = noopValue; // a transform that changes nothingThey 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.
What is not here
| Not shipped | Write |
|---|---|
nextTick | await Promise.resolve() |
nextMicrotask | queueMicrotask(fn) |
Queue, SmartQueue | the scheduler's lanes |
domScheduler, useScheduler | defaultScheduler |