Preventing double clicks in React, with Hooks

5 August 2024

This is a follow up to my previous post, about preventing double clicks in React, this time using React Hooks.

When building interactive web applications, it's crucial to prevent users from accidentally submitting forms or triggering actions multiple times. One effective and user-friendly approach is to disable the button immediately after it's clicked.

The Problem

Consider a form submission button:

function SubmitButton() {
  const handleSubmit = () => {
    // Perform some action, e.g., API call
    console.log("Form submitted!");
  };

  return <button onClick={handleSubmit}>Submit</button>;
}

If a user clicks this button twice quickly, the handleSubmit function will be called twice, potentially causing duplicate form submissions or API calls.

We can prevent double clicks by disabling the button immediately after it's clicked. Here's how we can implement this using React hooks:

import React from "react";

function submitForm() {
  // Simulating an async action that returns a Promise
  return new Promise((resolve) => setTimeout(resolve, 1000));
}

function SubmitButton() {
  const [isSubmitting, setIsSubmitting] = React.useState(false);

  const handleSubmit = async () => {
    if (isSubmitting) {
      return; // Prevent double submission
    }

    setIsSubmitting(true);

    try {
      // Perform some action, e.g., API call
      await submitForm();
      console.log("Form submitted successfully!");
    } catch (error) {
      console.error("Error submitting form:", error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <button onClick={handleSubmit} disabled={isSubmitting}>
      {isSubmitting ? "Submitting..." : "Submit"}
    </button>
  );
}

Let's break down this implementation:

  1. We use the useState hook to create an isSubmitting state variable, this lets us track when some background action is being run.
  2. The handleSubmit function:
    • Checks if the form is already submitting and returns early if it is.
    • Sets isSubmitting to true before starting the submission process.
    • Uses a try/catch block to handle the submission and any potential errors.
    • Sets isSubmitting back to false in the finally block, ensuring the button is re-enabled even if an error occurs.
  3. The button's disabled prop is tied to the isSubmitting state, so that the user can't click it again to trigger the action.
  4. The button text changes to provide feedback to the user.

We can also take this a step further, and create our own hook to extract this logic.

import React from 'react';

function submitForm() {
    // Simulating an async action that returns a Promise
    return new Promise(resolve => setTimeout(resolve, 1000))
}

export function useIsSubmitting(action): [loading, action] {
  const [loading, setLoading] = React.useState(false);

  function doAction(...x) {
    setLoading(true);
    return action(...x).finally(() => setLoading(false));
  }

  return [loading, doAction];
}

function SubmitButton() {
  const [isSubmitting, doHandleSubmit] = useIsSubmitting(submitForm);

  return (
    <button onClick={doHandleSubmit} disabled={isSubmitting}>
      {isSubmitting ? "Submitting..." : "Submit"}
    </button>
  );
}


The final component doesn't need to handle any logic for reseting isSubmitting, and can focus on rendering the different states.

Conclusion

Disabling the button after it's clicked is a simple yet effective way to prevent double clicks and double submissions in React applications. This approach provides clear visual feedback to the user and ensures that actions are only triggered once, even if the user accidentally clicks multiple times. By combining this with proper error handling and timeouts, you can create a robust and user-friendly interface for your forms and action buttons.