Skip to main content
useInputGuards sits between a text input’s raw events and an expensive onCommit callback — a network request, store update, or analytics event you don’t want firing on every keystroke. The visible value stays fully responsive while the hook decides when a value is worth committing. The “guards” in the name are the protections it layers over that callback:
  • Debounce — a burst of typing coalesces into a single commit with the final value.
  • Commit guard — commits are rate-limited to at most one per delay ms, across every input method.
  • Held-key suppression — repeated keydown from a held hotkey is dropped, so holding Enter or a paste shortcut doesn’t fire repeatedly.
  • Deduplication — the same value never commits twice in a row.
  • External sync — when the value prop changes from outside, the input re-syncs and any stale in-flight commit is cancelled.
By default all four user actions (typing, Enter, paste, clear) can commit; the triggers option lets you opt out of any of them.

Installation

npm install @servicetitan/anvil2-ext-common

Usage

Call the hook with the controlled value, a delay, and your onCommit handler, then spread the returned inputProps onto the input. The hook owns the visible value and the commit timing; your component just reacts to commits.
import { useInputGuards } from "@servicetitan/anvil2-ext-common";

function SearchBox({ query, onSearch }: { query: string; onSearch: (value: string) => void }) {
  const { inputProps } = useInputGuards({
    value: query,
    delay: 300,
    onCommit: onSearch,
  });

  return <input {...inputProps} />;
}
inputProps carries value, onChange, onPaste, and onKeyDown. Spread all of them — the paste and keydown handlers are what make the guards work; dropping them falls back to plain debounced typing.

Configuration

PropTypeDescription
valuestringThe controlled value. Changing it from outside re-syncs the visible input and cancels stale commits.
delaynumberDebounce and commit-guard window, in milliseconds.
onCommit(value: string) => voidCalled with a value worth committing — debounced, rate-limited, and deduplicated.
triggersCommitTrigger[]Which actions may commit. Omit (or pass empty) to enable all.

Choosing which actions commit

Pass triggers to restrict which user actions fire a commit. When omitted, all are active.
enum CommitTrigger {
  Typing = "typing", // debounced auto-commit after inactivity
  Enter = "enter",   // immediate commit on Enter
  Paste = "paste",   // immediate commit on paste
  Clear = "clear",   // immediate commit when cleared to ""
}
For example, to commit only on Enter, paste, and clear — never on a typing pause:
const { inputProps } = useInputGuards({
  value,
  delay: 300,
  onCommit: handleCommit,
  triggers: [CommitTrigger.Enter, CommitTrigger.Paste, CommitTrigger.Clear],
});

Behavior reference

Action / scenarioWhat happens
TypingVisible value updates instantly; onCommit fires after delay ms of inactivity.
PasteInserted at the cursor (or over the selection); commits immediately through the guard.
EnterCommits immediately if the guard is idle, otherwise defers until the guard window expires.
Clear to emptyonCommit('') fires immediately, bypassing the guard, and the guard is deactivated.
Rapid commitsFirst commits immediately; the next is deferred until delay ms have elapsed.
Held hotkeysRepeated keydown is suppressed, so a held Enter or paste shortcut only fires once.
Held arrow keysArrowLeft / ArrowRight are exempt, so the cursor keeps moving as expected.
Repeat valueA value identical to the last committed one is never committed again.
External value changeVisible value re-syncs, stale in-flight commits are cancelled, and the guard resets.
UnmountAll pending timers are cancelled; no trailing commit fires.
Last modified on June 22, 2026