useDebounced

Delay a value until typing stops — the one debounce the framework's own bindings use.

Every app that has a search box eventually writes the same six lines: a useState, a useEffect, a setTimeout, and a cleanup that clears it. Get the cleanup wrong and stale timers fire after unmount; get the dependency array wrong and the timer never restarts. useDebounced is that snippet, written once. useAsyncValidation and useQueryField are built on it, so a debounced subscription in your code behaves exactly like the framework's own.

import { useDebounced } from '@voltro/client'

const [term, setTerm] = useState('')
const debouncedTerm = useDebounced(term, 300)
const results = useSubscription('app', 'todos.search', { term: debouncedTerm })

The whole API is useDebounced(value, ms = 300). There is no options object — no leading edge, no maxWait, no flush() or cancel(). It is a trailing-edge debounce and nothing else. If you need one of those, you need a different primitive, not a flag on this one.

It is generic over the value, not limited to strings: useDebounced(filters) over an object works the same way.

Two behaviours worth knowing before you use it

The first value is not delayed. The internal state is seeded with value on the very first render, so the initial value is returned synchronously — including during SSR, where no timer ever fires. Only changes wait for the quiet window. That is what you want (no empty first paint), but it means you cannot use the "debounced value hasn't caught up yet" trick to detect the initial render.

Comparing the two is how you detect pending input, which is exactly what useAsyncValidation does internally to show its checking state:

const settling = term !== debouncedTerm

The value is compared by identity. value and ms are the effect's dependencies, so a value that is a fresh object or array on every render restarts the timer on every render. Pass primitives, or memoize:

const filters = useMemo(() => ({ status, assignee }), [status, assignee])
const debouncedFilters = useDebounced(filters, 300)

Changing ms restarts the quiet window too, for the same reason — so derive the delay from something stable rather than recomputing it inline.