Hooks

useDebouncedCallback

Debounce search or autosave actions using the latest callback and arguments; cancel pending work or run it immediately.

Import

import { useDebouncedCallback } from "@zentauri-ui/zentauri-components/hooks/useDebouncedCallback";

Demo source

Full client component used for this preview (same JSX as below). Adjust paths if you copy outside this app.

"use client";

import { useState } from "react";
import { useDebouncedCallback } from "@zentauri-ui/zentauri-components/hooks/useDebouncedCallback";
import { Button } from "@zentauri-ui/zentauri-components/ui/buttons";

export function DebouncedCallbackDemo() {
  const [draft, setDraft] = useState("");
  const [delay, setDelay] = useState(600);
  const [saved, setSaved] = useState<string | null>(null);
  const [saveCount, setSaveCount] = useState(0);
  const [message, setMessage] = useState("No changes scheduled.");
  const { debouncedCallback, cancel, flush, isPending } = useDebouncedCallback(
    (value: string) => {
      setSaved(value);
      setSaveCount((count) => count + 1);
      setMessage("Saved locally.");
    },
    delay,
  );

  return (
    <div className="space-y-5 rounded-xl border border-slate-300 p-5 text-slate-900 dark:border-slate-700 dark:text-slate-100">
      <h2 className="text-xl font-semibold">Debounced autosave</h2>
      <p className="text-sm text-slate-600 dark:text-slate-400">
        Type a draft and pause to save it. Rapid changes produce one save. This
        demo keeps the saved value on this page.
      </p>
      <label className="block text-sm">
        Draft
        <input
          value={draft}
          onChange={(event) => {
            const value = event.target.value;
            setDraft(value);
            debouncedCallback(value);
          }}
          placeholder="Write a draft title…"
          className="mt-2 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-white"
        />
      </label>
      <label className="block text-sm">
        Delay for the next change
        <select
          value={delay}
          onChange={(event) => setDelay(Number(event.target.value))}
          className="mt-2 block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 dark:border-slate-600 dark:bg-slate-900 dark:text-white"
        >
          {[300, 600, 1000].map((value) => (
            <option key={value} value={value}>
              {value} ms
            </option>
          ))}
        </select>
      </label>
      <div className="flex flex-wrap gap-3">
        <Button
          type="button"
          disabled={!isPending}
          onClick={() => {
            flush();
          }}
        >
          Save now
        </Button>
        <Button
          type="button"
          appearance="outline"
          disabled={!isPending}
          onClick={() => {
            cancel();
            setMessage("Pending save cancelled. Draft retained.");
          }}
        >
          Cancel pending save
        </Button>
      </div>
      <p role="status" className="text-sm">
        {isPending ? "Save scheduled. Waiting for typing to stop." : message}
      </p>
      <dl className="space-y-2 text-sm">
        <div className="flex flex-wrap gap-x-3">
          <dt className="font-medium">Saved draft:</dt>
          <dd className="min-w-0 wrap-anywhere" data-demo="saved-draft">
            {saved === null
              ? "Nothing saved yet"
              : saved === ""
                ? "(empty draft)"
                : saved}
          </dd>
        </div>
        <div className="flex gap-3">
          <dt className="font-medium">Completed saves:</dt>
          <dd data-demo="save-count" className="tabular-nums">
            {saveCount}
          </dd>
        </div>
      </dl>
      <p className="text-sm text-slate-600 dark:text-slate-400">
        Changing the delay keeps an already scheduled deadline. Cancel discards
        the scheduled call; it does not undo a completed save or stop an
        in-flight request.
      </p>
    </div>
  );
}

Debounced autosave

Type a draft and pause to save it. Rapid changes produce one save. This demo keeps the saved value on this page.

No changes scheduled.

Saved draft:
Nothing saved yet
Completed saves:
0

Changing the delay keeps an already scheduled deadline. Cancel discards the scheduled call; it does not undo a completed save or stop an in-flight request.

What it does

useDebouncedCallback(callback, delayMs) schedules trailing-edge actions. Each call replaces the arguments and restarts the timer. It complements useDebouncedValue: use the value hook for derived state and this hook for delayed actions such as search or autosave.

API notes

Returns { debouncedCallback, cancel, flush, isPending }. The three functions keep their identity across renders. flush() invokes pending work immediately and returns the callback result (including a Promise), or undefined when idle. cancel() discards pending work. isPending is reactive timer state; it becomes false when the callback starts, even if an async request is still running. The callback uses the latest committed closure and last scheduled arguments. A finite delay from 0 through 2,147,483,647 ms is required; zero is still asynchronous. A changed delay applies to future calls and preserves an existing deadline. Unmount discards work without flushing. This hook supports trailing calls only; it has no leading-call or max-wait option.

Common use cases

Schedule draft saves or delayed search actions after typing pauses. Flush on an explicit Save action and cancel when abandoning a draft. Handle async results and failures inside the callback; cancellation does not stop requests that have already begun.

Accessibility

Keep text input updates immediate and debounce the action. Label controls and announce meaningful save status with a polite live region. Do not debounce focus movement or essential keyboard feedback.

Next.js integration notes

Import from @zentauri-ui/zentauri-components/hooks/useDebouncedCallback in a client component. Invoke the controls from events or effects, never during render. Extract plain input values before scheduling rather than retaining event objects. No timers are started during server rendering. The hook does not persist pending work across navigation; use a deliberate Save action when data must be committed before leaving.

FAQ

Can I import this hook from a server component?

No. These hooks rely on browser APIs or React client lifecycle. Import them from a file marked with the "use client" directive or from a client child component.

Does the preview site bundle the same code as npm?

The component library app depends on the workspace package. Published builds resolve the same export paths under @zentauri-ui/zentauri-components.

Where should I validate accessibility for hook-driven UI?

Hooks do not replace semantics. Test focus order, labels, and keyboard flows in your real layout with assistive technologies you support.