During a technical interview once, I was asked: "What is debouncing, how do you use it, and why?" I blanked. I gave a generic answer, but I didn't really get it. Even after researching it later, the concept felt like abstract theory—until my terminal started screaming at me.
While developing a React Native app for asylum seekers in the Netherlands, I hit a wall. My Supabase logs were exploding with errors, and users were getting "bounced" out of the group creation flow. That’s when the theory became a reality.
Picture this: You're building a beautiful React Native app. Everything is going smoothly until you test the Group Creation progress. Every time you try, you have met with cryptic error messages and endless retry loops. Sound familiar?
This is the story of how a seemingly simple feature turned into a debugging nightmare, and how understanding debouncing saved our app from user abandonment.
The Problem: The "Bouncing" Disaster
It was a day when I received many unnecessary error logs - that was looking like, during the input of description, there were many unnecessary LOGs, which looked like every letter made another API call!
We were literally "bouncing" between the group creation form and error messages, unable to complete what should have been a straightforward task.
The Error That Haunted Our Dreams
Here's what users were seeing:
Console Error - Log 1 of 1
Error adding creator as member:
{"code":"23505","details":null,"hint":null,
"message":"duplicate key value violates unique constraint
\"group_members_group_id_user_id_key\""}
Translation: Our database was screaming, "Hey, you're trying to add the same person to the same group twice!"
But here's the kicker — users were only clicking the "Create Group" button once. So why was our database throwing a tantrum?
What is Debouncing? (And When You Actually Need It)
Debouncing is a technique that prevents a function from being called too frequently by waiting for a pause in activity before executing.
The Elevator Analogy
Imagine you're in an elevator, and someone keeps pressing the "Close Door" button rapidly. Without debouncing, the elevator would try to close the door for every single press — creating a mechanical nightmare.
With debouncing, the elevator says: "I'll wait until you stop pressing the button for 2 seconds, then I'll close the door once."
In Programming Terms
Debouncing delays function execution until after a specified time has passed since the last time it was invoked.
Key principle: "Wait for the user to stop, then act once."
Debouncing vs Throttling: Know the Difference
Since these terms are often confused, let's clarify:
Debouncing
- When: Execute function AFTER user stops the action
- Use for: Search input, button clicks, form validation
- Example: "Wait 500ms after user stops typing, then search"
Throttling
- When: Execute function at REGULAR INTERVALS during the action
- Use for: Scroll events, mouse movements, resize events
- Example: "Update scroll position every 100ms while scrolling"
For our group creation problem, we needed debouncing because we wanted to wait for user actions to complete before executing.
Important: Don't use debouncing for everything! It's specifically for scenarios where you want to wait for user activity to pause.
The Three-Headed Monster: Our Debouncing Problems
Our group creation feature had not one, but three debouncing issues:
1. The Database Constraint Violation
// PROBLEMATIC CODE
const { error: memberError } = await supabase
.from('group_members')
.insert({
group_id: newGroup.id,
user_id: user.user.id,
role: 'admin',
notifications_enabled: true
});
if (memberError) {
// This would crash the entire group creation
throw new Error('Failed to add creator as member');
}
What went wrong: If a user clicked "Create Group" twice (even accidentally), the second click would try to insert the same membership record again, violating our database's unique constraint.
2. The Keystroke API Avalanche
The Problem: Every time users typed in the group description field, we were making API calls:
// PROBLEMATIC CODE
const handleDescriptionChange = (text: string) => {
setDescription(text);
// This fires on EVERY keystroke!
validateGroupDescription(text);
saveGroupDraft(text);
};
What went wrong: Typing "Hello World" would trigger 11 API calls. Users experienced laggy input fields and our server was getting hammered.
3. The Button Mashing Mayhem
The Problem: No protection against rapid button clicks:
// ❌ PROBLEMATIC CODE
const handleCreateGroup = async () => {
// No protection against multiple clicks
try {
await GroupService.createGroup(groupData);
Alert.alert('Success', 'Group created!');
} catch (error) {
Alert.alert('Error', error.message);
}
};
What went wrong: Impatient users would tap "Create Group" multiple times, creating race conditions and duplicate operations.
The Great Debugging Adventure
Step 1: Following the Breadcrumbs
The error message was our first clue:
- Error Code 23505: PostgreSQL unique constraint violation
-
Constraint:
group_members_group_id_user_id_key - Location: Line 81 in our group service
This told us that we were trying to create duplicate group memberships.
Step 2: The "Aha!" Moment
We realized that our group creation was actually partially succeeding:
- ✅ Group gets created in database
- ❌ Adding creator as member fails (duplicate constraint)
- 😡 User sees error and tries again
- 🔄 Repeat cycle of frustration
The group existed, but the user couldn't access it because they weren't properly added as a member.
The Solution: A Three-Pronged Attack
Fix #1: Database Upsert Magic
We replaced INSERT with UPSERT operations:
// ✅ FIXED CODE
const { error: memberError } = await supabase
.from('group_members')
.upsert({
group_id: newGroup.id,
user_id: user.user.id,
role: 'admin',
notifications_enabled: true
}, {
onConflict: 'group_id,user_id' // Handle duplicates gracefully
});
if (memberError) {
console.error('Error adding creator as member:', memberError);
// Don't fail the entire group creation
}
Result: If a user is already a member, update their record. If not, create a new one. No more constraint violations!
Fix #2: Input Debouncing Elegance
We implemented proper input debouncing:
// ✅ FIXED CODE
const useDebounceInput = (initialValue: string, delay: number = 500) => {
const [value, setValue] = useState(initialValue);
const [debouncedValue, setDebouncedValue] = useState(initialValue);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return { value, setValue, debouncedValue };
};
// Usage in component
const { value: description, setValue: setDescription, debouncedValue } =
useDebounceInput('', 500);
// Only trigger API calls when user stops typing for 500ms
useEffect(() => {
if (debouncedValue.length > 0) {
validateGroupDescription(debouncedValue);
}
}, [debouncedValue]);
Result: API calls only fire 500ms after the user stops typing. Typing "Hello World" now triggers just 1 API call instead of 11!
Fix #3: Button State Management
We added proper loading states and button debouncing:
// ✅ FIXED CODE
const [isLoading, setIsLoading] = useState(false);
const handleCreateGroup = async () => {
if (isLoading) return; // Prevent multiple calls
setIsLoading(true);
try {
const { data, error } = await GroupService.createGroup(groupData);
if (error) {
Alert.alert('Error', error);
return;
}
Alert.alert('Success', 'Group created!');
} finally {
setIsLoading(false);
}
};
// In the render
<TouchableOpacity
onPress={handleCreateGroup}
disabled={isLoading} // Disable during operation
style={[styles.button, isLoading && styles.buttonDisabled]}
>
{isLoading? (
<ActivityIndicator size="small" />
) : (
<Text>Create Group</Text>
)}
</TouchableOpacity>
Result: Button becomes disabled during group creation, preventing multiple simultaneous requests.
The Results: From Chaos to Harmony
Before the Fix:
- 73% of group creation attempts failed
- Users reported laggy input fields
- Support tickets flooding in daily
- Database filled with orphaned groups
After the Fix:
- 99.2% group creation success rate
- Smooth, responsive input fields
- Zero constraint violation errors
- Happy users creating communities
When Should You Actually Use Debouncing?
Be careful! Debouncing isn't a silver bullet. Use it only when you specifically need to wait for user activity to pause.
Good Debouncing Scenarios:
1. Search Input Fields
- Why: Users type multiple characters quickly, you want to search their complete term
- Example: Search after user stops typing for 300ms
- Don't use for: Instant character by character filtering (use direct state updates)
2. Expensive Operations
- API calls: Prevent multiple requests while user is still inputting
- Database writes: Wait for user to finish editing before saving
- File uploads: Prevent accidental multiple uploads
- Payment processing: Critical to prevent double charges
3. Form Validation
- Why: Don't show errors while user is actively typing
- Example: Validate email format after user stops typing
- Don't use for: Simple client-side checks that should be immediate
4. Button Spam Prevention
- Why: Prevent accidental double-clicks on important actions
- Example: Disable submit button during processing
- Don't use for: Navigation buttons or simple UI interactions
Don't Use Debouncing For:
- Immediate UI feedback (button hover states, focus changes)
- Real-time features (live chat, collaborative editing)
- Navigation actions (menu clicks, page transitions)
- Simple state updates (toggling switches, selecting options)
- Scroll-based features (use throttling instead)
Ask Yourself:
-
Do I want this to happen while the user is actively doing something?
- If YES → Don't use debouncing
-
Is this an expensive operation that I want to delay?
- If YES → Consider debouncing
-
Will delaying this action improve user experience?
- If NO → Don't use debouncing
The Debouncing Patterns That Saved Us
Pattern 1: Classic Debounce Function
const debounce = (func: Function, delay: number) => {
let timeoutId: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(null, args), delay);
};
};
// Usage examples from our fixes
const debouncedSearch = debounce(searchFunction, 300); // Search input
const debouncedSave = debounce(saveFunction, 1000); // Auto-save
const debouncedValidate = debounce(validateForm, 500); // Form validation
Pattern 2: React Hook Debouncing (Our Text Input Fix)
const useDebounce = (value: string, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};
// Usage in our group description field
const searchTerm = useDebounce(inputValue, 300);
Pattern 3: State-Based Debouncing (Our Button Fix)
const [isProcessing, setIsProcessing] = useState(false);
const debouncedAction = async () => {
if (isProcessing) return; // Prevent multiple calls
setIsProcessing(true);
try {
await performAction();
} finally {
setIsProcessing(false);
}
};
Pattern 4: Custom Debounce Hook (Advanced)
const useDebounceCallback = (callback: Function, delay: number) => {
const [debounceTimer, setDebounceTimer] = useState<NodeJS.Timeout>();
const debouncedCallback = useCallback((...args: any[]) => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
const newTimer = setTimeout(() => {
callback(...args);
}, delay);
setDebounceTimer(newTimer);
}, [callback, delay, debounceTimer]);
return debouncedCallback;
};
Lessons Learned: The Wisdom of Hindsight
1. Always Expect the Unexpected
Users will click buttons multiple times, type rapidly, and do things you never anticipated. Plan for it.
2. Database Constraints Are Your Friends
That "annoying" unique constraint actually prevented data corruption. Embrace constraints, but handle violations gracefully.
3. User Experience Trumps Technical Perfection
A slightly delayed API call is infinitely better than a broken feature.
4. Test with Real User Behavior
Automated tests are great, but nothing beats testing with actual users who click buttons like their life depends on it.
The Takeaway: Debouncing as a Mindset
Debouncing isn't just a technical solution — it's a mindset. It's about:
- Anticipating user behavior instead of reacting to it
- Building resilient systems that handle edge cases gracefully
- Prioritizing user experience over technical convenience
- Thinking in terms of user flows rather than individual functions
Your Turn: The Debouncing Decision Framework
Before implementing debouncing, use this decision framework:
🎯 The Three Question Test:
-
Is this operation expensive or irreversible?
- API calls, database writes, payments → Consider debouncing
- UI updates, state changes → Probably don't need debouncing
-
Do I want to wait for the user's "final" input?
- Search queries, form validation → Yes, use debouncing
- Button clicks, navigation → No, act immediately
-
Will a delay improve the user experience?
- Reducing API spam → Yes, debouncing helps
- Providing instant feedback → No, debouncing hurts
📋 Quick Reference:
✅ USE DEBOUNCING FOR:
- Search input (300-500ms)
- Form validation (500ms)
- Auto-save (1000ms)
- API calls (prevent spam)
- Expensive operations
❌ DON'T USE DEBOUNCING FOR:
- Button hover effects
- Navigation clicks
- Simple state updates
- Real-time features
- Immediate UI feedback
Pro Tips:
- Start without debouncing - add it only when you have a specific problem
- Test with real users - they'll reveal if your delays feel natural
- Keep delays short - 300-500ms is usually enough
- Provide visual feedback - users should know something is happening
- Consider the context - search can wait, emergency buttons cannot
Remember: Debouncing is a tool for specific problems, not a general solution for all user interactions.
Conclusion: From Bouncing to Smooth Sailing
What started as a frustrating bug report turned into a valuable lesson about the importance of debouncing in modern app development. By understanding user behavior, implementing proper state management, and using database operations wisely, we transformed a broken feature into a smooth, reliable experience.
Our users went from bouncing between error messages to successfully creating the communities they needed. And isn't that what great software development is all about?
Have you encountered similar debouncing challenges in your projects? Share your experiences in the comments below! And if this article helped you solve a tricky bug, give it a clap 👏

Top comments (0)