Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Rate limiting

debounce and throttle are two small, framework-agnostic functions that control how often a callback actually runs, each solving a different shape of that problem (see "Choosing between them" below). They matter most paired with a field that has its own loading state, like eun-search or eun-select: the field has no idea what the request behind it actually looks like, so deciding when to fire it, and how often, is the consumer's own responsibility, wired around the field's eunchange event.

npm install @eunomia/elements
import { debounce, throttle } from "@eunomia/elements";

Debounce

Delays calling fn until delay milliseconds have passed since the last call, and every call resets the timer. The classic use is a live search field: fire one request after the user pauses typing, instead of one per keystroke.

function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay?: number, // default 400
): (...args: Parameters<T>) => void;
Parameter Type Default Description
fn Function The function to delay
delay number 400 Milliseconds to wait after the last call before it fires

Type in the field below and watch the timer bar: every keystroke snaps it back to empty and restarts it, so it only ever completes, and the call actually fires, once you pause for a full 400ms.

Keystrokes 0 Debounced calls 0

debounce must be called exactly once per field, not rebuilt on every render: a fresh call creates a fresh timer, so recreating it on every render resets that timer just as often, and the debounce never actually gets to complete. Pick a framework in the toolbar above to see where that "created once" guarantee comes from in each one:

import { debounce } from "@eunomia/elements";

const field = document.querySelector("eun-input");

const runSearch = debounce((query: string) => {
  fetchResults(query);
}, 400);

field.addEventListener("eunchange", () => runSearch(field.value));

See eun-search's own "Debouncing a real request" example (its Loading section) for the full pattern combined with a loading state and a modal results panel.

Throttle

Runs fn at most once every time milliseconds, no matter how often it's called, since unlike debounce, it doesn't wait for a pause. Suited to a continuous stream of events with no natural "settled" moment: scroll, resize, drag, mousemove.

function throttle<T extends (...args: any[]) => any>(
  fn: T,
  time?: number, // default 400
  options?: { leading?: boolean },
): (...args: Parameters<T>) => void;
Parameter Type Default Description
fn Function The function to rate-limit
time number 400 Minimum delay, in ms, between two calls
options.leading boolean false Call fn immediately on the first call, instead of only on trailing edge

Click rapidly and watch the cooldown bar: it starts full, empties the instant a click actually fires, then refills over the throttle window, gating every rapid click underneath it until it's full again.

Click me rapidly
Clicks 0 Throttled calls 0

The same "created exactly once" rule from Debounce above applies here too, plus one more thing to get right in a component: attaching and detaching the listener itself, so it doesn't pile up a new one on every render:

import { throttle } from "@eunomia/elements";

const runOnScroll = throttle(() => {
  updateScrollProgress();
}, 200);

window.addEventListener("scroll", runOnScroll);

Choosing between them

Neither is "better": they solve different shapes of problem. The question is whether the events you're reacting to have a natural pause to wait for.

Reach for… When… Typical case
Debounce The events have a natural "done, for now" moment worth waiting for Live search/autocomplete, validate-while-typing, auto-save a draft
Throttle The events are continuous, with no pause to wait for Scroll, resize, mousemove, drag handlers

Both default to it, but tune the delay against how fast your own event fires and how expensive the call behind it actually is.

See also