Skip to content

Debounce

Use when: you want to collapse rapid user input (search-as-you-type, resize, window scroll) into one deferred action to avoid hammering the server or doing expensive work on every keystroke. Don’t use when: the action must fire immediately on the first event and you only want to throttle subsequent ones — use throttle instead.

Store the debounce timer as a private field on the Cubit and cancel it before scheduling a new one. No external debounce library is needed.

class
class SearchCubit
SearchCubit
extends
class Cubit<S extends object = any, Args = void, Deps extends object = Record<string, never>>

Cubit<S> is a StateContainer<S> with emit / patch exposed as public mutation surface. Today it adds nothing structurally beyond StateContainer — both are inherited from the underlying StructuralContainer<S>. Kept as a real class (not a type alias) because downstream code does instance instanceof Cubit checks (see A2 audit).

The class body is intentionally empty: a no-op emit override would still go through applyState, and patch is inherited from StructuralContainer (path-diffed, microtask-flushed). If a caller needs the old "skip if no real change" patch semantics, they can wrap patch themselves or call emit after a manual equality check.

Cubit
<
interface SearchState
SearchState
> {
// Timer handle for the pending debounced call.
private
SearchCubit.debounceTimer: number | null
debounceTimer
:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

Obtain the return type of a function type

ReturnType
<typeof
function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number
setTimeout
> | null = null;
private
SearchCubit.requestId: number
requestId
= 0;
constructor() {
super({
SearchState.query: string
query
: '',
SearchState.results: SearchResult[]
results
: [],
SearchState.status: "idle" | "loading" | "success" | "error"
status
: 'idle',
SearchState.error: string | null
error
: null });
// Cancel any in-flight timer when the instance is disposed.
this.
StateContainer<SearchState, void, Record<string, never>>.onSystemEvent: <"dispose">(event: "dispose", handler: SystemEventHandler<SearchState, "dispose">) => (() => void)
onSystemEvent
('dispose', () => {
if (this.
SearchCubit.debounceTimer: number | null
debounceTimer
!== null)
function clearTimeout(id: number | undefined): void
clearTimeout
(this.
SearchCubit.debounceTimer: number
debounceTimer
);
});
}
SearchCubit.setQuery: (query: string) => void
setQuery
= (
query: string
query
: string) => {
// Update the input field immediately so the UI feels responsive.
this.
StateContainer<SearchState, void, Record<string, never>>.patch(partial: {
query?: string | undefined;
results?: readonly {
id?: string | undefined;
title?: string | undefined;
}[] | undefined;
status?: "idle" | "loading" | "success" | "error" | undefined;
error?: string | null | undefined;
}): void

Override of StructuralContainer.patch that routes through the StateContainer concerns: disposed guard, dev-only emit-rate check, _changedWhileHydrating flag, pending-change capture (so legacy listeners and stateChanged system events see the merged prev/next), and the registry-level stateChanged notification. We still call super.patch so path-marking semantics (the whole point of patch) are preserved.

patch
({
query?: string | undefined
query
,
status?: "idle" | "loading" | "success" | "error" | undefined
status
: 'idle' });
// Cancel the previous pending search.
if (this.
SearchCubit.debounceTimer: number | null
debounceTimer
!== null)
function clearTimeout(id: number | undefined): void
clearTimeout
(this.
SearchCubit.debounceTimer: number
debounceTimer
);
if (!
query: string
query
.
String.trim(): string

Removes the leading and trailing white space and line terminator characters from a string.

trim
()) {
this.
StateContainer<SearchState, void, Record<string, never>>.patch(partial: {
query?: string | undefined;
results?: readonly {
id?: string | undefined;
title?: string | undefined;
}[] | undefined;
status?: "idle" | "loading" | "success" | "error" | undefined;
error?: string | null | undefined;
}): void

Override of StructuralContainer.patch that routes through the StateContainer concerns: disposed guard, dev-only emit-rate check, _changedWhileHydrating flag, pending-change capture (so legacy listeners and stateChanged system events see the merged prev/next), and the registry-level stateChanged notification. We still call super.patch so path-marking semantics (the whole point of patch) are preserved.

patch
({
results?: readonly {
id?: string | undefined;
title?: string | undefined;
}[] | undefined
results
: [],
status?: "idle" | "loading" | "success" | "error" | undefined
status
: 'idle' });
return;
}
// Schedule the actual fetch after 300 ms of silence.
this.
SearchCubit.debounceTimer: number | null
debounceTimer
=
function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number
setTimeout
(() => {
void this.
SearchCubit.fetchResults: (query: string) => Promise<void>
fetchResults
(
query: string
query
);
}, 300);
};
private
SearchCubit.fetchResults: (query: string) => Promise<void>
fetchResults
= async (
query: string
query
: string) => {
const
const reqId: number
reqId
= ++this.
SearchCubit.requestId: number
requestId
;
this.
StateContainer<SearchState, void, Record<string, never>>.patch(partial: {
query?: string | undefined;
results?: readonly {
id?: string | undefined;
title?: string | undefined;
}[] | undefined;
status?: "idle" | "loading" | "success" | "error" | undefined;
error?: string | null | undefined;
}): void

Override of StructuralContainer.patch that routes through the StateContainer concerns: disposed guard, dev-only emit-rate check, _changedWhileHydrating flag, pending-change capture (so legacy listeners and stateChanged system events see the merged prev/next), and the registry-level stateChanged notification. We still call super.patch so path-marking semantics (the whole point of patch) are preserved.

patch
({
status?: "idle" | "loading" | "success" | "error" | undefined
status
: 'loading',
error?: string | null | undefined
error
: null });
try {
const
const results: SearchResult[]
results
= await
const api: {
search(q: string): Promise<SearchResult[]>;
}
api
.
function search(q: string): Promise<SearchResult[]>
search
(
query: string
query
);
if (
const reqId: number
reqId
!== this.
SearchCubit.requestId: number
requestId
) return; // superseded by a newer call
this.
StateContainer<SearchState, void, Record<string, never>>.patch(partial: {
query?: string | undefined;
results?: readonly {
id?: string | undefined;
title?: string | undefined;
}[] | undefined;
status?: "idle" | "loading" | "success" | "error" | undefined;
error?: string | null | undefined;
}): void

Override of StructuralContainer.patch that routes through the StateContainer concerns: disposed guard, dev-only emit-rate check, _changedWhileHydrating flag, pending-change capture (so legacy listeners and stateChanged system events see the merged prev/next), and the registry-level stateChanged notification. We still call super.patch so path-marking semantics (the whole point of patch) are preserved.

patch
({
results?: readonly {
id?: string | undefined;
title?: string | undefined;
}[] | undefined
results
,
status?: "idle" | "loading" | "success" | "error" | undefined
status
: 'success' });
} catch (
function (local var) e: unknown
e
) {
if (
const reqId: number
reqId
!== this.
SearchCubit.requestId: number
requestId
) return;
this.
StateContainer<SearchState, void, Record<string, never>>.patch(partial: {
query?: string | undefined;
results?: readonly {
id?: string | undefined;
title?: string | undefined;
}[] | undefined;
status?: "idle" | "loading" | "success" | "error" | undefined;
error?: string | null | undefined;
}): void

Override of StructuralContainer.patch that routes through the StateContainer concerns: disposed guard, dev-only emit-rate check, _changedWhileHydrating flag, pending-change capture (so legacy listeners and stateChanged system events see the merged prev/next), and the registry-level stateChanged notification. We still call super.patch so path-marking semantics (the whole point of patch) are preserved.

patch
({
results?: readonly {
id?: string | undefined;
title?: string | undefined;
}[] | undefined
results
: [],
status?: "idle" | "loading" | "success" | "error" | undefined
status
: 'error',
error?: string | null | undefined
error
:
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
function (local var) e: unknown
e
) });
}
};
}
function SearchBox() {
const [state, search] = useBloc(SearchCubit);
return (
<div>
<input
value={state.query}
onChange={(e) => search.setQuery(e.target.value)}
placeholder="Search…"
/>
{state.status === 'loading' && <p>Searching…</p>}
<ul>
{state.results.map((r) => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
}
  • Async — request-id guard and cancellation with AbortController
  • System Eventsdispose event for cleanup