Fixing Continuous Re-Renders in React: Understanding Passing by Reference vs. Passing by Value and application of spreed operator
Introduction
Recently, while working on a React project, I encountered an issue where my component was re-rendering continuously despite implementing a debounce hook(It is just a fancy useEffect
hook wraped with some additional logic). This led to unnecessary API calls and performance issues. After debugging, I realized the problem was due to how dependencies were passed in the useDebounce
hook. In this post, I'll explain the issue, why it happened, and how I fixed it.
The Problem: Continuous Re-Renders in React
I had implemented a custom useDebounce
hook to delay API calls when a user changed filters or/and change the name in my dashboard. Here’s what the initial debounce hook looked like:
export const useDebounce = (callback: () => void, dependencies: any[], delay: number = 500) => {
useEffect(() => {
let timerId = setTimeout(callback, delay);
return () => clearTimeout(timerId);
}, [dependencies, delay]);
};
And here’s how I used it in my component:
useDebounce(fetchUsers, [
name,
dependenciy1,
dependenciy2,
dependenciy3,
], 500);
The expectation was that fetchUsers
would be debounced and only trigger after 500ms when any of the dependencies changed. However, my component kept re-rendering infinitely, causing multiple API requests.
Why Did This Happen?
The issue was with passing an array (dependencies
) as a dependency in the useEffect
hook. To understand the problem we have to know about Passing by Reference vs. Passing by value.
Understanding Passing by Reference vs. Passing by Value
Primitive values (numbers, strings, booleans) are passed by value, meaning a copy is created when they are assigned or passed as function arguments. So, if a dependency is a primitive value, React can correctly determine if it has changed.
Objects, arrays, and functions are passed by reference in JavaScript. This means that even if the contents of an array remain the same, creating a new array results in a new reference in memory
How This Affects useEffect
Since arrays are reference types, React treats
{dependencies}
as a new array on every render.Even if the content of
dependencies
remains unchanged, React sees it as a new dependency.This causes
useEffect
to run on every render, leading to infinite API calls.
The Fix: Spreading Dependencies Properly
To fix this, I have make the pass the values to pass the dependencies I spread the dependencies instead of passing them as a single array. Here’s the updated debounce hook:
export const useDebounce = (callback: () => void, dependencies: any[], delay: number = 500) => {
useEffect(() => {
const timerId = setTimeout(callback, delay);
return () => clearTimeout(timerId);
}, [...dependencies, delay]); // Spread dependencies properly
};
The spread operator (...
) allows us to break an array into individual elements, ensuring React tracks each value independently rather than treating the entire array as a new reference.
Now, useEffect
only runs when an actual value inside the dependencies array changes, preventing unnecessary re-renders.
Why the Solution Works
Ensures that
useEffect
runs only when individual values change, not on every render.Prevents React from treating dependencies as a new reference by spreading them instead of passing an array.
Significantly improves performance by reducing unnecessary API calls.
Conclusion
If you ever face a situation where a debounce hook or/and a useEffect hook is causing infinite re-renders, check if you’re passing an array as a dependency in useEffect
. Instead, spread the dependencies correctly to ensure React only re-runs the effect when necessary. This simple fix can help optimize performance and avoid unnecessary API calls!