Frontend Development 9 min read

How to Stop Duplicate Uploads: UI Locks, Logic Flags, and Best Practices

This article explains why repeatedly clicking an upload button can cause duplicate requests, UI confusion, and data errors, and presents three robust solutions—disabling the button, using a state flag, and why debounce or throttle are unsuitable—plus essential backend safeguards.

JavaScript
JavaScript
JavaScript
How to Stop Duplicate Uploads: UI Locks, Logic Flags, and Best Practices

Why Repeated Clicks Are Dangerous

In modern web apps, clicking the "Upload" button usually triggers an asynchronous operation such as

fetch

or

XMLHttpRequest

that sends a

POST

request.

async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  console.log("开始上传...");
  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData,
  });
  console.log("上传完成!");
  return response.json();
}

const uploadButton = document.getElementById('upload-btn');
const fileInput = document.getElementById('file-input');

uploadButton.addEventListener('click', () => {
  if (fileInput.files.length > 0) {
    uploadFile(fileInput.files[0]);
  }
});

The code calls

uploadFile

every time the button is clicked, so five rapid clicks generate five independent upload requests.

Solution 1: Simple UI‑Level Lock (Disable Button)

Disable the button when the upload starts and re‑enable it when the async task finishes, providing clear visual feedback.

Implementation steps :

Set the button’s

disabled

attribute to

true

immediately after the click.

In the

finally

block of the async operation, set

disabled

back to

false

.

Wrap the upload logic in a

try...catch...finally

structure to guarantee the button is always restored.

Pros : Simple, intuitive for users.

Cons : If JavaScript execution is delayed by a few milliseconds, an extremely fast user might click twice before the button is disabled.

Solution 2: Logic‑Level Lock with a State Flag

Introduce a flag such as

isUploading

that lives outside the UI and acts as a logical lock.

Implementation steps :

Declare a variable

let isUploading = false;

(global or within a closure).

At the start of the click handler, check

isUploading

. If it is

true

, return early.

If it is

false

, set it to

true

and proceed with the upload.

In the

finally

block, reset

isUploading

to

false

and also re‑enable the button.

let isUploading = false; // state flag

uploadButton.addEventListener('click', async () => {
  // 1. Check flag
  if (isUploading) {
    console.log('已有任务在上传中,请勿重复点击。');
    return;
  }
  if (fileInput.files.length === 0) return;

  // 2. Set flag and disable UI
  isUploading = true;
  uploadButton.disabled = true;
  uploadButton.textContent = '上传中...';

  try {
    await uploadFile(fileInput.files[0]);
    alert('上传成功!');
  } catch (error) {
    console.error('上传失败:', error);
    alert('上传失败,请重试。');
  } finally {
    // 3. Reset flag and UI
    isUploading = false;
    uploadButton.disabled = false;
    uploadButton.textContent = '上传文件';
  }
});

Pros : Logically rigorous; works even if the UI lock fails.

Cons : Requires manual state management, but it is essential for critical operations.

Solution 3: Debounce and Throttle (Why They’re Not Ideal Here)

Debounce waits for a pause before executing, which delays the upload and hurts user expectation. Throttle limits execution to a fixed interval, but if the interval is shorter than the upload duration, multiple uploads can still occur. Therefore, a state‑flag approach is more precise for guaranteeing a single active upload.

Don’t Forget the Backend: The Final Defense

Frontend restrictions can be bypassed with developer tools or scripts. The backend must enforce its own safeguards, such as:

Idempotency : Use a unique request ID (Idempotency‑Key) to detect and discard duplicates.

Database constraints : Unique indexes on file names or hashes.

Distributed locks : Lock resources (e.g., user‑ID + file name) during processing.

Putting It All Together

Core logic : Use a state flag (

isUploading

) as a logical lock.

User feedback : Disable the button and update its label or icon while the task runs.

Robust code : Wrap the upload in

try...catch...finally

to always reset state and UI.

Security baseline : Implement backend validation; frontend checks alone are insufficient.

By layering these defenses, you can build an upload feature that is both user‑friendly and reliable, handling even the most frantic clickers gracefully.

FrontendJavaScriptDebouncethrottleuploadstate-flagui-lock
JavaScript
Written by

JavaScript

Provides JavaScript enthusiasts with tutorials and experience sharing on web front‑end technologies, including JavaScript, Node.js, Deno, Vue.js, React, Angular, HTML5, CSS3, and more.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.