Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟢 Beginner🔵 Advanced
🎪 Event Handling Basics🖱️ Click Events⌨️ Keyboard Events📝 Form Events🎯 Event Handlers with Parameters🚫 Event Default Behavior🔗 Event Delegation⚡ Performance & Events
Reactjs/Events/Click Events

🖱️ Optimized Click Handlers — Advanced Click Patterns

Production applications require sophisticated click handling: debouncing rapid clicks, detecting double-clicks reliably, and preventing button spam. Master advanced click patterns.


🎯 Debouncing and Throttling Click Handlers

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>

💡 Reliable Double-Click Detection

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>
  );
}

🔧 Handling Rapid Clicks (Request Deduplication)

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>

🎨 Advanced Button State Management

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>
  );
}

🔑 Key Takeaways

  • ✅ Implement debouncing for save/search operations to prevent API spam
  • ✅ Use throttling for high-frequency scroll/resize events
  • ✅ Build custom double-click detection for complex click patterns
  • ✅ Implement request deduplication to prevent duplicate submissions
  • ✅ Use AbortController to cancel in-flight requests when appropriate
  • ✅ Manage button states (loading, success, error) for better UX
  • ✅ Leverage useCallback and useRef for efficient click handling

Ready to practice? Keyboard Events (Advanced) | Challenges


Resources

Python Docs

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

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn