Summary#
debounce: is a property of the control, written on the call. It coalesces that control's event handlers: the handler
runs once, after the events stop for the given number of milliseconds.
Input(value: draft.Slug, onInput: CheckSlug, debounce: 300);Type acme quickly and CheckSlug runs once, 300ms after the last keystroke — not four times.
Signature#
debounce: <milliseconds> // a non-negative whole number; applies to this element's event handlers- It needs an event handler to coalesce (
onInput,onClick, …). Adebounce:with no handler is a compile error — it would do nothing, silently. - The handler runs with the latest value: the field's binding is written before the handler runs, so a handler that reads the field it is bound to sees what the user just typed.
Description#
Some handlers are cheap and want to fire on every event. Some ask a question that costs a round-trip — is this name
available?, what matches this search? — and firing those per keystroke is wasteful and racy. debounce: is how you
say "wait until they stop."
What debounce does not do. It does not deduplicate answers. If a handler asks the server something, a slow reply for an earlier value can still land after a faster reply for a later one. When the answer is written into state, guard it: re-read the field after the call and drop the answer if it no longer describes what's in the box (the example below does this). Debounce reduces the number of questions; it does not order the answers.
It is not validation. A friendly as-you-type check is a courtesy. The rules that actually protect your data are the
ones declared on the entity — [Unique], [Required], [Pattern] — and those are enforced when the data is saved,
whatever the client did or didn't check first.
Examples#
The is-this-name-taken check. An ordinary server function answers the question; an ordinary action asks it and writes
the answer into state; debounce: makes it one round-trip per pause in typing:
entity Organization {
[Required, MaxLength(100), Unique, Pattern("^[a-z0-9]+(-[a-z0-9]+)*$")] string Slug;
string Name;
}
// An ordinary server function — it just answers a question.
bool CheckSlugAvailability(string candidate) {
if (candidate == "") { return true; }
return !Organization.Any(o => o.Slug == candidate);
}
[Page("/org/new")]
[Render(CSR)]
component OrgCreatePage() {
Organization draft;
bool slugFree = true;
on mount { draft = new Organization {}; }
// An action may write state and may call a server function. Re-reading the field after the call drops a stale
// answer — the reply for a slug the user has already typed past.
action CheckSlug() {
var candidate = draft.Slug;
var free = CheckSlugAvailability(candidate);
if (candidate == draft.Slug) { slugFree = free; }
}
render {
Stack(gap: 4) {
Input(value: draft.Name, placeholder: "Organization name");
Input(value: draft.Slug, placeholder: "team-slug", onInput: CheckSlug, debounce: 300);
if (!slugFree) { Text("That slug is taken."); }
}
}
}A search box wants the same thing — one query per pause, not one per letter:
Input(value: term, onInput: Search, debounce: 250);See also#
- component —
actionmembers, and why an action (not anon changeblock) is what asks a question like this. - control — foreign UI controls (charts, grids, maps) — the controls an event handler can hang off, and their event props.
- creating & saving data — binding an input to an entity field, and
UnitOfWork.Commit().