
ReactJS
Production applications require sophisticated click handling: debouncing rapid clicks, detecting double-clicks reliably, and preventing button spam. Master advanced click patterns.
Debouncing delays execution until the user stops clicking. Throttling executes at most once per interval. These techniques prevent API spam and improve performance.
Debouncing is ideal for search or save operations where you want a single execution after user stops. Throttling is better for scroll events or resize handlers where you want periodic updates.
// Debounce implementation
function useDebounce<T extends (...args: any[]) => any>(
callback: T,
delay: number
): T {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
return useCallback(
(...args: any[]) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
},
[callback, delay]
) as T;
}
// Throttle implementation
function useThrottle<T extends (...args: any[]) => any>(
callback: T,
delay: number
): T {
const lastRunRef = useRef<number>(0);
return useCallback(
(...args: any[]) => {
const now = Date.now();
if (now - lastRunRef.current >= delay) {
lastRunRef.current = now;
callback(...args);
}
},
[callback, delay]
) as T;
}
// Usage: debounced save button
function AutoSaveForm() {
const [content, setContent] = useState("");
const [saved, setSaved] = useState(false);
// Debounced API call: only executes after 1s of inactivity
const debouncedSave = useDebounce(async (text: string) => {
try {
await fetch("/api/save", {
method: "POST",
body: JSON.stringify({ content: text }),
});
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} catch (error) {
console.error("Save failed:", error);
}
}, 1000);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
setContent(value);
debouncedSave(value);
};
return (
<div>
<textarea value={content} onChange={handleChange} />
{saved && <p style={{ color: "green" }}>Saved!</p>}
</div>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: Throttled scroll handler
function ThrottledScrollIndicator() {
const [scrollY, setScrollY] = useState(0);
const throttledScroll = useThrottle((e: Event) => {
setScrollY((e.target as Window).scrollY);
}, 100);
useEffect(() => {
window.addEventListener("scroll", throttledScroll);
return () => window.removeEventListener("scroll", throttledScroll);
}, [throttledScroll]);
return <div>Scroll position: {scrollY}</div>;
}
// Example 3: Request-based debounce (cancel previous request)
function SearchWithAbort() {
const [results, setResults] = useState([]);
const abortControllerRef = useRef<AbortController | null>(null);
const debouncedSearch = useDebounce(
async (query: string) => {
// Cancel previous request
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: abortControllerRef.current.signal,
});
const data = await response.json();
setResults(data);
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
console.error("Search failed:", error);
}
}
},
300
);
return (
<input
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search..."
/>
);
}</details>
Detecting double-clicks reliably requires tracking click timing. Simple `onDoubleClick` doesn't work in all scenarios (especially with controlled components).
interface UseDoubleClickOptions {
onSingleClick: () => void;
onDoubleClick: () => void;
delay?: number;
}
function useDoubleClick({
onSingleClick,
onDoubleClick,
delay = 300,
}: UseDoubleClickOptions) {
const clickCountRef = useRef(0);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
return useCallback(() => {
clickCountRef.current += 1;
if (clickCountRef.current === 1) {
timeoutRef.current = setTimeout(() => {
if (clickCountRef.current === 1) {
onSingleClick();
}
clickCountRef.current = 0;
}, delay);
} else if (clickCountRef.current === 2) {
clearTimeout(timeoutRef.current!);
onDoubleClick();
clickCountRef.current = 0;
}
}, [onSingleClick, onDoubleClick, delay]);
}
// Usage: toggling edit mode with double-click
function EditableTitle() {
const [isEditing, setIsEditing] = useState(false);
const [title, setTitle] = useState("Click title");
const handleDoubleClick = useDoubleClick({
onSingleClick: () => console.log("Single click"),
onDoubleClick: () => setIsEditing(true),
});
if (isEditing) {
return (
<input
autoFocus
value={title}
onChange={(e) => setTitle(e.target.value)}
onBlur={() => setIsEditing(false)}
onKeyDown={(e) => {
if (e.key === "Enter") setIsEditing(false);
}}
/>
);
}
return (
<h1 onClick={handleDoubleClick} style={{ cursor: "pointer" }}>
{title}
</h1>
);
}Rapid clicking can trigger multiple API requests. Implement request deduplication to ensure only one request executes.
// Request deduplication using cache
function useRequestDeduplication() {
const requestMapRef = useRef(new Map<string, Promise<any>>());
return useCallback(
async <T,>(key: string, fetcher: () => Promise<T>): Promise<T> => {
// Return existing promise if request already in flight
if (requestMapRef.current.has(key)) {
return requestMapRef.current.get(key)!;
}
// Execute new request
const promise = fetcher();
requestMapRef.current.set(key, promise);
try {
const result = await promise;
return result;
} finally {
// Remove from cache after completion
requestMapRef.current.delete(key);
}
},
[]
);
}
// Usage: button that prevents duplicate submissions
function SubmitButton() {
const [loading, setLoading] = useState(false);
const deduplicate = useRequestDeduplication();
const handleSubmit = async () => {
setLoading(true);
try {
await deduplicate("form-submit", async () => {
const response = await fetch("/api/submit", {
method: "POST",
});
if (!response.ok) throw new Error("Submit failed");
return response.json();
});
} catch (error) {
console.error("Submit error:", error);
} finally {
setLoading(false);
}
};
return (
<button onClick={handleSubmit} disabled={loading}>
{loading ? "Submitting..." : "Submit"}
</button>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: Multi-click gesture detection (click 3 times rapidly)
function useMultiClick(clicksRequired: number = 3, delay: number = 500) {
const clickCountRef = useRef(0);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
return useCallback(
(callback: () => void) => {
clickCountRef.current += 1;
if (clickCountRef.current === clicksRequired) {
callback();
clickCountRef.current = 0;
return;
}
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
clickCountRef.current = 0;
}, delay);
},
[clicksRequired, delay]
);
}
// Usage: triple-click to trigger special action
function TripleClickElement() {
const handleTripleClick = useMultiClick(3, 500);
return (
<div onClick={() => handleTripleClick(() => console.log("Triple clicked!"))}>
Click 3 times rapidly
</div>
);
}</details>
Sophisticated button handlers manage loading states, error states, and disabled states across async operations.
interface AsyncButtonState {
idle: "idle";
loading: "loading";
success: "success";
error: "error";
}
type AsyncState = AsyncButtonState["idle" | "loading" | "success" | "error"];
function useAsyncButton(
handler: () => Promise<void>,
successDuration: number = 2000
) {
const [state, setState] = useState<AsyncState>("idle");
const [error, setError] = useState<Error | null>(null);
const executeAsync = useCallback(async () => {
setState("loading");
setError(null);
try {
await handler();
setState("success");
setTimeout(() => setState("idle"), successDuration);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
setState("error");
}
}, [handler, successDuration]);
return { state, error, execute: executeAsync };
}
// Usage: button with loading, success, and error states
function SaveButton() {
const { state, error, execute } = useAsyncButton(async () => {
const response = await fetch("/api/save", { method: "POST" });
if (!response.ok) throw new Error("Save failed");
});
const getButtonText = () => {
switch (state) {
case "loading":
return "Saving...";
case "success":
return "Saved!";
case "error":
return `Error: ${error?.message}`;
default:
return "Save";
}
};
const getButtonColor = () => {
switch (state) {
case "loading":
return "gray";
case "success":
return "green";
case "error":
return "red";
default:
return "blue";
}
};
return (
<button
onClick={execute}
disabled={state === "loading"}
style={{ background: getButtonColor(), color: "white" }}
>
{getButtonText()}
</button>
);
}Ready to practice? Keyboard Events (Advanced) | Challenges
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward