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
| Prop | Type | Description |
|---|
value | string | The controlled value. Changing it from outside re-syncs the visible input and cancels stale commits. |
delay | number | Debounce and commit-guard window, in milliseconds. |
onCommit | (value: string) => void | Called with a value worth committing — debounced, rate-limited, and deduplicated. |
triggers | CommitTrigger[] | 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 / scenario | What happens |
|---|
| Typing | Visible value updates instantly; onCommit fires after delay ms of inactivity. |
| Paste | Inserted at the cursor (or over the selection); commits immediately through the guard. |
| Enter | Commits immediately if the guard is idle, otherwise defers until the guard window expires. |
| Clear to empty | onCommit('') fires immediately, bypassing the guard, and the guard is deactivated. |
| Rapid commits | First commits immediately; the next is deferred until delay ms have elapsed. |
| Held hotkeys | Repeated keydown is suppressed, so a held Enter or paste shortcut only fires once. |
| Held arrow keys | ArrowLeft / ArrowRight are exempt, so the cursor keeps moving as expected. |
| Repeat value | A value identical to the last committed one is never committed again. |
External value change | Visible value re-syncs, stale in-flight commits are cancelled, and the guard resets. |
| Unmount | All pending timers are cancelled; no trailing commit fires. |
Last modified on June 22, 2026