When a React List Page Scrolls, API Calls Fire in Bursts—Is It Because the Object in the useEffect Dependency Array Is Recreated Every Time?
When APIs are requested repeatedly on a list page, the cause is usually not backend rate limiting or request-library retries; it is an object, array, or inline function created on every render that has been placed in the useEffect dependency array. React compares dependencies by reference equality. Each render of a parent or the component itself can produce a new reference, so the effect is judged to have changed dependencies. If the effect also calls setState, it can form a render—new reference—run again—render again loop. In typical 2026 projects, first distinguish whether the reference changes every round or the effect reads a stale value, then decide whether to narrow dependencies, stabilize references, or change the structure. This is usually more reliable than simply emptying the dependency array.
React compares references, not contents
Two separately created {} with exactly the same fields are still two different objects to React. A developer says 'the value did not change'; React concludes 'the reference changed'. These two statements are not at the same level. Common triggers include object literals, array literals, inline arrow functions, a configuration object passed down wholesale from a parent, and return values from filter, map, and sort. They are not coding mistakes; they simply produce a new reference on every render.
To judge whether the problem escalates into 'it never stops', check whether the effect writes back: only with setState, dispatch, or updates to an external store is a closed loop likely. Without write-back, the usual symptom is running several extra times. Investigating these two types together easily wastes time in the wrong direction.
Diagnose with Three Checks and One Verification: find the source before changing code
Duplicate requests have two sources: the reference changes every round, or the effect reads a stale value. Distinguishing the source first reduces rework such as 'the request count is right, but data does not update after changing filters'.
- Check 1: whether the dependencies contain reference types: objects, arrays, inline functions, props passed down from a parent, and filter/map outputs. Mark them if they match.
- Check 2: whether the effect writes back: only setState, dispatch, or writing to an external store can create a closed loop.
- Check 3: the ratio of requests to renders: in the Network panel, see whether the number of requests grows with render count, or appears in bursts when typing, scrolling, or switching tabs.
- One verification: temporarily replace dependencies with primitive values such as id or status, or move constants outside the component, and see whether requests converge to the expected count. Confirm causality before deciding on an approach.
Verification is only for confirming causality. Constants moved outside the component cannot read props or state. Warnings from dependency-checking rules are clues themselves; first think clearly about whether you are adding a value or a reference, and do not rush to add it.
Four approaches and cost comparison (experience range)
There is more than one path to the same goal—making the effect run at the expected time. The differences lie in change size, readability, and the risk of later breakage. The list below is ordered by how often projects encounter these cases; costs are typical ranges, not precise quotes.
- Narrow dependencies to primitive values: split the object into fields such as id, page, and status. The change is usually small, and it suits cases where the dependency needs only a few fields. With many fields, the dependency array becomes longer; add a comment explaining the source.
- Stabilize references with useMemo / useCallback: cache the object or function on the producing side. This suits objects that must be passed wholesale to child components or shared by multiple effects. The pitfall is that if the memo's own dependencies are still new objects, the cache fails and the problem is only moved up one level.
- Store the latest value in useRef + empty dependencies: suitable for effects such as tracking, logging, or reading configuration only once on mount, which do not need to rerun when the value changes. If the business requires a new request whenever filters change, this approach leaves data stale.
- Extract a custom hook as a single entry point: encapsulate request, dependency convergence, and race handling once and reuse them in multiple places. Suitable for mid-to-back-office projects with more than 5 similar requests. The cost is maintaining an extra abstraction layer, and newcomers must read the source first.
Estimating for a moderately complex list page (about 3–6 related effects): converging dependencies plus regression is typically about 0.5–1.5 person-days; adding race handling and unmount cleanup takes about another half day; building a custom hook for the first time costs about half a day to a day more, but it amortizes when reused in more than 5 places. When planning, reserve time by this range rather than estimating as 'changing one line of dependencies'.
On delivery: common exposure points when the integration window is short
In typical 2026 delivery schedules, it is not rare for the integration testing window to be only 1–2 days and for the API team to rate-limit by call frequency. Once the list page is mounted, switching tabs and pulling down fire requests in bursts, backend logs are flooded, and integration work is forced to stop to locate 'who is hammering the API'. The practice is to run a dependency self-check before release, converge dependencies to primitive values such as id plus page number, and add an incrementing sequence number at the request site to discard stale responses when they return. The result is usually about half a day more for regression, in exchange for fewer firefighting calls due to rate limiting in the first week after release. Locating this type of page typically takes about half an hour to half a day, and remediation is typically about 0.5–1.5 person-days.
Another easily misjudged situation is development mode: with StrictMode enabled, effects are intentionally run twice. This is expected behavior, used to expose missing cleanup functions. Seeing two requests only in development and one in production is normal. If production still repeats, go back to reference comparison and write-back inside the effect.
Applicable and non-applicable boundaries
Dependency convergence is worth doing, but not every effect should be handled by the same standard. The principle is not 'the more complete the dependencies, the better'; it is whether the cost of one effect rerun is acceptable. Rerunning to rebuild only local objects and rerunning to hit APIs, write logs, or trigger tracking have different priorities.
- Worth being strict about: data requests, subscriptions and unsubscriptions, tracking reports, third-party SDK initialization, and effects that write server state.
- No need to be strict about: inline functions used only in local events, not included in dependencies and not passed to memoized child components; initialization that runs only on mount and does not read props; one-off calculations in purely presentational components.
Boundary statement: If an effect only subscribes and cleans up, and does not read external variables that can change, leaving dependencies empty is reasonable. Conversely, having reference types in dependencies while requiring 'run only once' is inherently contradictory; change the structure instead of continuing to add memo.
FAQ
If I put an object in dependencies and wrap it with useMemo, will duplicate requests stop?
Not necessarily. If useMemo's dependencies still contain objects or functions created every render, the cache fails and requests continue. First confirm that its dependencies are primitive values, then check the request count.
In development, the same API is requested twice. Is that caused by React re-rendering?
With StrictMode enabled, effects are intentionally run twice, which is expected behavior. If production still repeats, go back to reference comparison and write-back inside the effect.
Can emptying the dependency array reduce requests for now?
It can reduce requests, but the effect reads the values from the first render, so data may not update after filters or pagination change. It is suitable for temporary diagnosis, not as a long-term approach.
If a request fires on every keystroke in an input, is that also a dependency mistake?
Mostly it is not a reference problem; the input value is in dependencies without debouncing. For reference problems, change the dependency structure; for high-frequency input, add debouncing or switch to requesting on submit.
If you have a page with abnormal request counts, first use Three Checks and One Verification to confirm causality, then decide whether to narrow dependencies, stabilize references, or change the structure. Change only one place at a time, and after each change check the Network panel and page-switch warnings. This method applies to effects centered on data requests; purely presentational components or one-time initialization do not need the same standard of rework.
-
Uni-app Cross-Platform Development Stuck Halfway in 2026: What to Check First
Date: Aug 22, 2026 Read: 122
-
In 2026, with keep-alive preserving filters and scroll position on a list page, should you refetch every time you return from detail when the data is still stale?
Date: Oct 22, 2026 Read: 7
-
In 2026, the API returns 0 for inventory but the page shows “--”: is the frontend treating 0 as no value?
Date: Oct 20, 2026 Read: 14
-
In 2026, why does a modal's gray overlay cover only the card while the surrounding page is still clickable—is the overlay just not filling the screen?
Date: Oct 19, 2026 Read: 19
-
Is it still worth fixing keyboard focus in AI-generated popups and forms before launch in 2026?
Date: Oct 18, 2026 Read: 19




