| Stale-Closures-in-React-Hooks :👈 | 👉:React-QA-1 |
How to Fix Infinite API Requests in React |
How to Fix Infinite API Requests in React
1. Quick Answer
Infinite API requests in React occur when a component repeatedly calls an API without stopping. This commonly happens when useEffect dependencies are incorrect, state updates trigger repeated effects, or a component continuously remounts.
Common solutions:
- Provide the correct dependency array to
useEffect. - Avoid updating state unnecessarily inside an effect.
- Use stable references for objects and functions used as dependencies.
- Move event-triggered API calls into event handlers when appropriate.
- Cancel or ignore obsolete requests during effect cleanup.
- Check whether the component is repeatedly mounting and unmounting.
- Use a data-fetching library when caching, deduplication, and request management are needed.
2. Table of Contents
- What Are Infinite API Requests?
- Common Causes
- How the Infinite Request Loop Happens
- Example 1: Missing Dependency Array
- Example 2: Object Dependencies
- Example 3: Function Dependencies
- Example 4: Updating State Inside useEffect
- Example 5: Calling an API from an Event Handler
- Example 6: Cleaning Up API Requests
- Example 7: Component Repeatedly Mounting
- Understanding React Strict Mode
- Common Mistakes
- Best Practices
- React Interview Questions
- Summary
3. What Are Infinite API Requests?
Infinite API requests happen when React repeatedly executes code that sends an HTTP request, causing the same endpoint to be called again and again without an intended stopping condition.
For example, a component fetches a list of users. After receiving the response, it updates state. The state update triggers another render, which runs the effect again. The effect sends another request, updates state again, and the cycle continues.
This can lead to:
- Excessive API calls.
- Slow application performance.
- Unnecessary server load.
- Repeated network traffic.
- Rate-limit errors.
- Outdated responses overwriting newer data.
4. Common Causes of Infinite API Requests
| Cause | Explanation | Solution |
|---|---|---|
| Missing dependency array | The effect runs after every render. | Add the appropriate dependency array. |
| Incorrect dependencies | A dependency changes after every render. | Stabilize the dependency or restructure the effect. |
| State update inside an effect | The state update triggers another render, which can trigger the effect again. | Update state only when necessary and use correct dependencies. |
| New object or array dependencies | A new reference is created on every render. | Use primitive dependencies or stabilize the reference where appropriate. |
| New function dependencies | A function declared in the component is recreated on every render. | Move the function inside the effect or stabilize it when needed. |
| Incorrect event handling | An API request is made during rendering instead of in an effect or event handler. | Call the API from the appropriate lifecycle or user event. |
| Repeated component mounting | A parent condition or changing key repeatedly remounts the component. | Fix the rendering condition or key. |
| Request retries | Retry logic repeatedly retries failed requests without appropriate limits. | Use bounded retries and suitable backoff. |
5. How the Infinite Request Loop Happens
The loop occurs when the state update causes another render and the effect's dependencies cause the effect to run again.
6. Example 1: Missing Dependency Array
Incorrect Code
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then(response => response.json())
.then(data => setUsers(data));
});
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default Users;
Problem: The effect has no dependency array, so it runs after every render. If the request updates users, the component renders again and sends another request.
Correct Code
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
}
fetchUsers().catch(error => {
console.error("Error fetching users:", error);
});
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default Users;
Why this works: The empty dependency array means the effect does not rerun merely because the component's state changes. It runs when the component mounts, subject to React Strict Mode behavior in development.
Important: An empty dependency array is appropriate only when the effect does not need to respond to changing reactive values. If the effect uses props or state that can change, those values generally need to be dependencies.
7. Example 2: Object Dependencies
Objects and arrays are compared by reference in React dependency arrays. Creating a new object during every render gives it a new reference, even when its contents are identical.
Incorrect Code
import { useEffect, useState } from "react";
function Products() {
const [products, setProducts] = useState([]);
const filters = {
category: "books"
};
useEffect(() => {
fetch("/api/products?category=" + filters.category)
.then(response => response.json())
.then(data => setProducts(data));
}, [filters]);
return <div>Products: {products.length}</div>;
}
export default Products;
Problem: Every render creates a new filters object. React sees a different dependency reference, reruns the effect, and sends another request.
Solution 1: Use Primitive Dependencies
useEffect(() => {
async function fetchProducts() {
const response = await fetch(
"/api/products?category=books"
);
if (!response.ok) {
throw new Error("Failed to fetch products");
}
const data = await response.json();
setProducts(data);
}
fetchProducts().catch(console.error);
}, []);
This version is suitable when the category is fixed and no reactive values need to be included in the dependency array.
Solution 2: Depend on the Actual Primitive Value
const category = "books";
useEffect(() => {
async function fetchProducts() {
const response = await fetch(
"/api/products?category=" +
encodeURIComponent(category)
);
if (!response.ok) {
throw new Error("Failed to fetch products");
}
const data = await response.json();
setProducts(data);
}
fetchProducts().catch(console.error);
}, [category]);
Because category is a primitive string, the dependency remains equal as long as its value does not change.
If the category comes from props or state, include it in the dependency array. For more complex filters, consider constructing the object inside the effect from its primitive dependencies.
8. Example 3: Function Dependencies
A function declared inside a component is normally recreated on every render. If an effect depends on that function, the effect can rerun after every render.
Incorrect Code
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
async function fetchUsers() {
const response = await fetch("/api/users");
const data = await response.json();
setUsers(data);
}
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
return <div>Users: {users.length}</div>;
}
export default Users;
Problem: The render creates a new fetchUsers function. The dependency changes, causing the effect to run again. Updating users triggers another render, which creates another function and can restart the cycle.
Solution 1: Move the Function Inside the Effect
useEffect(() => {
async function fetchUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
}
fetchUsers().catch(console.error);
}, []);
This is a good solution when the function is used only by that effect and does not depend on changing reactive values.
Solution 2: Use useCallback When the Function Must Be Shared
import { useCallback, useEffect, useState } from "react";
function Users({ category }) {
const [users, setUsers] = useState([]);
const fetchUsers = useCallback(async () => {
const response = await fetch(
"/api/users?category=" +
encodeURIComponent(category)
);
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
}, [category]);
useEffect(() => {
fetchUsers().catch(console.error);
}, [fetchUsers]);
return <div>Users: {users.length}</div>;
}
export default Users;
Why this works: The function reference remains stable while its dependencies remain unchanged. When category changes, the callback changes and the effect correctly fetches data for the new category.
Do not use useCallback automatically for every function. First determine whether the function actually needs to be a dependency and whether moving it inside the effect would be simpler.
9. Example 4: Updating State Inside useEffect
Updating state inside an effect is not inherently wrong. It becomes a problem when the update causes a dependency to change on every render, which then triggers the effect again.
Incorrect Code
import { useEffect, useState } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [requestCount, setRequestCount] = useState(0);
useEffect(() => {
fetch("/api/users")
.then(response => response.json())
.then(data => {
setUsers(data);
setRequestCount(requestCount + 1);
});
}, [requestCount]);
return <div>Users: {users.length}</div>;
}
export default UserList;
Problem: The effect depends on requestCount, then increments it after each request. That state update changes a dependency and triggers another request indefinitely.
Correct Code
import { useEffect, useState } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [requestCount, setRequestCount] = useState(0);
useEffect(() => {
async function fetchUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
setRequestCount(count => count + 1);
}
fetchUsers().catch(console.error);
}, []);
return (
<div>
<p>Users: {users.length}</p>
<p>Requests completed: {requestCount}</p>
</div>
);
}
export default UserList;
Why this works: The effect does not depend on requestCount, so incrementing that counter does not cause another request.
The functional updater avoids relying on a captured counter value. In a real application, use an appropriate loading and error state as well, and include any reactive values that the request actually depends on.
10. Example 5: Calling an API from an Event Handler
If an API request should happen only after a user performs an action, place the request in the event handler rather than using an effect that watches unrelated state.
Example
import { useState } from "react";
function SearchUsers() {
const [query, setQuery] = useState("");
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
async function handleSearch() {
if (!query.trim()) {
setUsers([]);
setError("Enter a search term.");
return;
}
setLoading(true);
setError("");
try {
const response = await fetch(
"/api/users?q=" + encodeURIComponent(query)
);
if (!response.ok) {
throw new Error("Search request failed");
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
return (
<div>
<input
value={query}
onChange={event => setQuery(event.target.value)}
placeholder="Search users"
/>
<button onClick={handleSearch} disabled={loading}>
Search
</button>
{loading && <p>Loading...</p>}
{error && <p>{error}</p>}
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export default SearchUsers;
Why this works: The request is triggered by the user's search action, not by every render. Typing into the input updates the query state without automatically sending a request.
Use this approach when: The request is a direct response to a user action, such as clicking Search, submitting a form, or selecting a button.
Use an effect when: The request synchronizes the component with changing reactive values, such as fetching a user's profile when the user ID changes.
11. Example 6: Cleaning Up API Requests
Cleanup does not, by itself, fix an effect that intentionally runs after every render. However, it helps prevent obsolete requests from continuing or updating state after the result is no longer relevant.
Example Using AbortController
import { useEffect, useState } from "react";
function SearchUsers({ query }) {
const [users, setUsers] = useState([]);
const [error, setError] = useState("");
useEffect(() => {
if (!query.trim()) {
setUsers([]);
setError("");
return;
}
const controller = new AbortController();
async function fetchUsers() {
try {
const response = await fetch(
"/api/users?q=" + encodeURIComponent(query),
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
setError("");
} catch (err) {
if (err.name !== "AbortError") {
setError(err.message);
}
}
}
fetchUsers();
return () => {
controller.abort();
};
}, [query]);
return (
<div>
{error && <p>{error}</p>}
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export default SearchUsers;
Why this works:
- The effect fetches users when
querychanges. - When the query changes or the component unmounts, cleanup aborts the previous fetch.
- An aborted request is handled separately from an ordinary error.
- Obsolete requests are less likely to waste resources or produce outdated results.
Important: Aborting a browser request does not guarantee that the server has stopped processing it. Server-side cancellation and request handling depend on the API implementation.
12. Example 7: Component Repeatedly Mounting
Sometimes the effect dependencies are correct, but the component itself is repeatedly unmounted and mounted. An effect that fetches data on mount will then run again.
Incorrect Parent Code
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Update
</button>
<UserList key={count} />
</div>
);
}
Problem: The key changes whenever count changes. React treats UserList as a different component instance, unmounts the old instance, and mounts a new one. Its mount effect runs again.
Correct Code
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Update
</button>
<UserList />
</div>
);
}
Use a stable key that represents the component's identity. Changing a key is appropriate when you deliberately want React to reset that component's state and lifecycle, but it should not be used as a general mechanism to force updates.
13. Understanding React Strict Mode
In development, React Strict Mode may run an effect's setup, cleanup, and setup again when a component mounts. This helps detect effects that are not safe to repeat or do not clean up correctly.
For example, a mount effect that fetches data may appear to send two requests in development when Strict Mode is enabled.
This behavior is different from a true infinite request loop.
| Behavior | Possible Explanation |
|---|---|
| Two initial effect executions in development | Strict Mode's development-only checks may be responsible. |
| A new request after every state update | The effect may be running after every render or depending on a value that keeps changing. |
| Requests continue even when state appears unchanged | Check object or function dependencies, remounting, retries, and other request triggers. |
| Requests occur after navigation or parent updates | The component may be remounting, or a dependency may have changed. |
Do not disable Strict Mode just to hide duplicate requests. First check the effect dependencies and cleanup logic. If duplicate network traffic matters, consider request cancellation, caching, or deduplication.
14. Common Mistakes
| Mistake | Why It Is Wrong | Better Approach |
|---|---|---|
| Leaving out the dependency array | The effect runs after every render. | Specify the appropriate dependencies. |
| Using an empty array for every effect | The effect may ignore changes in props or state that should trigger another request. | Include all required reactive dependencies. |
| Adding a newly created object to dependencies | Its reference changes on every render. | Use primitive dependencies or construct the object inside the effect. |
| Using a function dependency without stabilizing it | A new function reference can retrigger the effect after each render. | Move the function inside the effect or use useCallback when appropriate. |
| Updating a dependency inside the effect | The update may trigger the effect repeatedly. | Review whether that state needs to be a dependency at all. |
| Making API calls during rendering | Rendering may happen more than once, so requests can be repeated. | Use an effect or an event handler, depending on the intended behavior. |
| Ignoring failed requests | Retries or repeated state changes may continue without clear error handling. | Handle errors and use bounded retries where needed. |
| Disabling Strict Mode immediately | This can hide lifecycle problems instead of fixing them. | Make effects safe to repeat and implement cleanup. |
| Using changing component keys | React repeatedly remounts the component. | Use stable keys that represent component identity. |
15. Best Practices to Prevent Infinite API Requests
- Choose dependencies carefully: An effect should rerun when the reactive values it relies on change.
- Keep effects focused: Separate unrelated synchronization tasks into separate effects when doing so improves clarity.
- Avoid unnecessary state: Do not store values in state when they can be derived directly from existing props or state.
- Use functional state updates: When updating state based on its previous value, use an updater function where appropriate.
- Use stable references thoughtfully: Use
useMemooruseCallbackonly when reference stability is needed. - Handle cleanup: Cancel obsolete fetches and clear timers or subscriptions created by effects.
- Separate user actions from synchronization: Use event handlers for requests that should happen because the user clicked a button or submitted a form.
- Prevent obsolete responses: Ensure older requests cannot overwrite newer results when the request context changes.
- Use request deduplication or caching when needed: A data-fetching library can help manage repeated requests, caching, retries, and loading states.
- Inspect network activity: Browser developer tools can help identify which endpoint is being called repeatedly and how often.
- Follow the Hooks dependency lint rules: Fix dependency issues rather than suppressing warnings without understanding them.
16. React Interview Questions
Q1. Why does React repeatedly call an API inside useEffect?
A common reason is that the effect runs after every render because it has no dependency array. Another possibility is that a dependency changes on every render or the component repeatedly remounts.
Q2. What is the difference between useEffect() and useEffect(..., [])?
An effect without a dependency array runs after every render. An effect with an empty dependency array does not rerun merely because state changes, although development Strict Mode can run its setup more than once to check effect behavior.
Q3. Can setState inside useEffect cause an infinite loop?
Yes, if the state update changes a dependency that causes the same effect to run again, repeatedly. Updating state inside an effect is not inherently wrong; the problem is an uncontrolled feedback loop.
Q4. Why can an object dependency cause repeated API requests?
React compares dependencies using identity. A newly created object has a different reference from the object created during the previous render, even when their properties are identical.
Q5. How can useCallback help prevent repeated requests?
useCallback can preserve a function reference while its dependencies remain unchanged. This can prevent an effect from rerunning because of a newly created function reference. The callback must still have the correct dependencies.
Q6. Does React Strict Mode cause infinite API requests?
Strict Mode can run effect setup and cleanup an additional time during development. It does not, by itself, explain a continuous request loop. Check dependencies, remounting, and retry behavior to identify the actual cause.
Q7. How do you prevent an old API response from overwriting new data?
Use an effect cleanup strategy, such as AbortController or an ignore flag, so obsolete requests cannot update the current UI state.
Q8. When should you use an event handler instead of useEffect?
Use an event handler when a request should happen because of a specific user action. Use an effect when a request synchronizes the component with changing reactive values or an external system.
Q9. How do you debug repeated API calls?
Inspect the browser's Network panel, identify which component triggers the request, check the effect dependency array, examine object and function references, and verify whether the component is remounting.
Q10. How can a data-fetching library help?
Libraries such as TanStack Query can provide caching, request deduplication, loading and error states, and controlled refetching. They reduce the amount of manual request-management code but do not eliminate the need to configure query keys and refetch behavior correctly.
17. Summary
- Infinite API requests usually result from an effect repeatedly running because of incorrect dependencies or repeated component mounting.
- Always inspect the
useEffectdependency array. - Avoid creating new object or function dependencies on every render when stable references are required.
- Do not update a dependency inside an effect without understanding the resulting feedback loop.
- Use event handlers for requests triggered by user actions.
- Use cleanup and cancellation to handle obsolete requests.
- Understand the difference between Strict Mode's development checks and a genuine infinite loop.
- Use caching or request deduplication when repeated requests are expected to share the same data.
Golden Rule: An API request should run only when the intended trigger occurs. Identify that trigger, make the effect dependencies reflect it, and prevent unnecessary state changes or component remounts from repeatedly triggering the request.
| Stale-Closures-in-React-Hooks :👈 | 👉:React-QA-1 |