How do you stop a debounced input handler from using stale state in a real UI?

Yo folks, I’m wiring up a search box with keyboard shortcuts and I’m trying to debounce the input handler, but I keep hitting a weird failure mode where the handler runs with stale state (like an old query or old filter) after a fast sequence of key events.

function setupSearch(input, getState, onSearch) {
  let t;

  input.addEventListener("input", (e) => {
    const value = e.target.value;

    clearTimeout(t);
    t = setTimeout(() => {
      // sometimes uses old filters when user toggles them quickly
      onSearch({ query: value, filters: getState().filters });
    }, 250);
  });
}

What pattern do you use to debounce DOM input events without stale closures or racey state reads when other UI events can change state in between?

1 Like

Your value is fine, but the “old filters” thing usually happens because getState() is giving you a snapshot that isn’t actually current at fire-time (or it’s mutating under you). The pattern I use is: keep “latest state” in a mutable ref that every UI event updates immediately, and have the debounced callback only read from that ref right before calling onSearch. Something like:

function setupSearch(input, stateRef, onSearch) {
  let t;
  input.addEventListener("input", (e) => {
    clearTimeout(t);
    t = setTimeout(() => {
      onSearch({ query: input.value, filters: stateRef.current.filters });
    }, 250);
  });
}

Naming tip: I literally call it latestStateRef so I don’t pretend it’s “state” in the React sense.

1 Like