Close Menu
ToolTechBlogToolTechBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    How to Source Passive Candidates with AI in 2026

    October 28, 2026

    How to Teach Content Strategy to Stakeholders Who Don’t Speak Marketing

    October 28, 2026

    Reddit is the latest platform to embrace the ‘For You’ feed

    October 28, 2026
    Facebook X (Twitter) Instagram
    ToolTechBlogToolTechBlog
    • Home
    • AI Tools
    • Web Hosting
    • Tech
    • Digital Marketing
    • Business Software
    • VPN & Cybersecurity
    ToolTechBlogToolTechBlog
    Home»Web Hosting»Timers, Listeners, and Fetch Races
    Web Hosting

    Timers, Listeners, and Fetch Races

    Tool Tech TeamBy Tool Tech TeamOctober 20, 2026No Comments10 Mins Read
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr WhatsApp Email
    Timers, Listeners, and Fetch Races
    Share
    Facebook Twitter LinkedIn Pinterest Telegram Email

    React useEffect Cleanup Patterns for Timers, Listeners, and Fetch Races

    SitePoint Team

    SitePoint TeamPublished inJavaScript·Programming·Web·
    October 19, 2026

    The AI briefing for Developers

    Stay up to date with AI tools, model releases, and developer workflows that matter.

    Weekly. Free. One click to leave.

    SitePoint Premium
    Stay Relevant and Grow Your Career in Tech

    • Premium Results
    • Publish articles on SitePoint
    • Daily curated jobs
    • Learning Paths
    • Discounts to dev tools

    7 Day Free Trial. Cancel Anytime.

    Code examples require React 18+. StrictMode double-invocation behavior was introduced in React 18.

    Missed cleanup functions in React useEffect are among the most common sources of memory leaks, stale-state bugs, and race conditions. Typical beginner tutorials demonstrate the happy path: fetch data, set state, render. They rarely address what happens when a component unmounts mid-flight, when a dependency changes faster than an async operation resolves, or when event listeners silently pile up across re-renders. This article provides a unified mental model for useEffect cleanup across three categories (timers, listeners, and fetch races), a reference table mapping setup to cleanup to pitfall, and a copy-paste PR review checklist that development teams can adopt immediately.

    Table of Contents

    How useEffect Cleanup Actually Works

    The Setup-Cleanup Lifecycle in One Sentence

    The mental model is simple: React runs the setup function when the component mounts, runs the cleanup function when the component unmounts, and on every dependency change, runs cleanup for the previous render’s effect before running setup for the current render’s effect. This cleanup-then-setup cycle on dependency change is the part most developers either overlook or misunderstand. It means that every effect invocation is paired with a corresponding teardown, and React guarantees that ordering. The setup function acquires resources; the cleanup return releases them. The dependency array declares which values, when changed, trigger that cycle again.

    Why StrictMode Double-Invokes Your Effects

    React 18’s StrictMode intentionally runs each component’s effect twice in development by simulating an unmount and remount: setup → cleanup → setup. Your setup function executes twice on the initial render in development. This double invocation exists specifically to surface missing cleanup logic. If a component registers an event listener in setup but never removes it in cleanup, StrictMode will produce two listeners after the simulated remount, making the bug immediately visible during development. As documented in the React docs on StrictMode (https://react.dev/reference/react/StrictMode), this behavior only occurs in development mode and does not affect production builds. However, the bugs it exposes are entirely real. A component that behaves incorrectly under StrictMode’s double-mount has a latent cleanup bug that will manifest in production whenever the component remounts due to navigation, conditional rendering, or parent re-renders.

    A component that behaves incorrectly under StrictMode’s double-mount has a latent cleanup bug that will manifest in production whenever the component remounts due to navigation, conditional rendering, or parent re-renders.

    import{ useEffect }from'react';functionExampleComponent({ dependency }){useEffect(()=>{console.log('Effect setup with dependency:', dependency);return()=>{console.log('Effect cleanup for dependency:', dependency);};},[dependency]);return<div>Anatomy of useEffect</div>;}

    Pattern 1: Cleaning Up Timers (setInterval / setTimeout)

    The Bug: Stacking Intervals on Re-render

    When a setInterval starts inside a useEffect that depends on a prop or state value, every time that dependency changes, React re-runs the effect. Without cleanup, the previous interval continues running. A second interval starts. Then a third. The observable symptom depends on what the interval does: a counter increments N times per tick (where N is the number of orphaned intervals), a polling callback fires multiple times per tick, or a UI animation speeds up erratically. Each orphaned interval holds a closure over stale state, compounding the problem. Under StrictMode’s double-mount in development, this bug surfaces immediately as two intervals running from the very first render.

    The Fix: clearInterval in the Cleanup Return

    The correct pattern creates the interval in setup and clears it in cleanup. When the dependency changes, React clears the old interval before starting a new one with the updated value. This guarantees exactly one active interval at any point in time.

    import{ useEffect, useState }from'react';functionPollingComponent({ delay }){const[count, setCount]=useState(0);useEffect(()=>{if(!delay || delay <=0||!Number.isFinite(delay))return;const id =setInterval(()=>{setCount(c=> c +1);}, delay);return()=>{clearInterval(id);};},[delay]);return<div>Count:{count}</div>;}

    The same principle applies to setTimeout. Replace setInterval with setTimeout and clearInterval with clearTimeout. Store the timer ID returned by the scheduling function, then clear it in the cleanup return.

    Pattern 2: Cleaning Up Event Listeners

    The Bug: Listener Pile-up on Window/Document

    Adding an event listener to window, document, or any DOM node outside the component’s own render tree requires manual cleanup. Without a matching removeEventListener in the cleanup return, every re-render (or StrictMode double-mount) attaches an additional handler. The handlers accumulate. A resize listener that updates state will trigger setState multiple times per resize event, once for each orphaned listener. This degrades performance and can cause subtle state corruption when multiple handlers race to update the same state variable.

    The Fix: Matching add and remove with the Same Reference

    The critical detail is that removeEventListener must receive the exact same function reference passed to addEventListener. An inline arrow function created inside the effect body produces a new reference on every invocation, so calling removeEventListener with a different inline arrow does nothing. The listener is never removed. Declare the handler as a named function (or a variable holding the function) inside the effect, then pass that same variable to both addEventListener and removeEventListener.

    The critical detail is that removeEventListener must receive the exact same function reference passed to addEventListener.

    import{ useEffect, useState }from'react';functionWindowSizeTracker(){const[width, setWidth]=useState(typeofwindow!=='undefined'?window.innerWidth:0);useEffect(()=>{if(typeofwindow==='undefined')return;functionhandleResize(){setWidth(window.innerWidth);}window.addEventListener('resize', handleResize);return()=>{window.removeEventListener('resize', handleResize);};},[]);return<div>Window width:{width}px</div>;}

    If the handler depends on a prop or state value and is defined outside the effect (for example, with useCallback), that handler must appear in the dependency array. When the reference changes, React cleans up the old listener and attaches the new one. Note that re-adding the listener on each dependency change is correct behavior: the old handler (with stale closure) is removed and the new handler (with fresh values) is attached.

    Pattern 3: Cancelling Fetch Race Conditions with AbortController

    The Bug: Stale Responses Overwriting Fresh State

    Consider a search component where the user types quickly. Each keystroke triggers a new fetch. The request for “rea” might take 300ms while the request for “react” takes 50ms. The “react” response arrives first, and the UI shows the correct results. Then the “rea” response arrives, and because nothing prevented it from calling setState, it overwrites the UI with stale, incorrect results. This is a classic race condition. The slower response wins not because it is more recent, but because it resolves later. Without cancellation, there is no mechanism to tell the browser to discard an in-flight request whose results are no longer relevant.

    The Fix: AbortController Signal in Cleanup

    The modern solution is AbortController. Each effect invocation creates a new controller instance. The controller’s signal property is passed to fetch as an option. When the cleanup function runs (because the dependency changed or the component unmounted), it calls controller.abort(), which cancels the in-flight request. The fetch promise rejects with an AbortError. Browsers generally attempt to cancel the underlying network request as well, though the behavior varies by engine (see the Fetch spec on AbortSignal and MDN’s browser compatibility tables). This guarantees that only the most recent request can successfully set state.

    Handling the AbortError Gracefully

    Calling abort() rejects the promise. This is normal, not an error condition. The catch block must distinguish AbortError from genuine network failures. Without this guard, the UI would flash an error for every intentional cancellation, or the rejection would trigger error-logging infrastructure. The standard check inspects the error’s name property: if it equals 'AbortError', the catch block returns early without updating state or surfacing an error.

    import{ useEffect, useState }from'react';functionSearchResults({ query }){const[results, setResults]=useState([]);const[error, setError]=useState(null);const[loading, setLoading]=useState(false);useEffect(()=>{if(!query){setResults([]);setError(null);setLoading(false);return;}const controller =newAbortController();asyncfunctionfetchResults(){setLoading(true);setError(null);try{const response =awaitfetch(`/api/search?q=${encodeURIComponent(query)}`,{signal: controller.signal,});if(!response.ok){thrownewError(`HTTP${response.status}`);}const data =await response.json();const items =Array.isArray(data?.items)? data.items:[];setResults(items);}catch(err){if(err.name==='AbortError'){return;}setError(err.message);}finally{setLoading(false);}}fetchResults();return()=>{controller.abort();};},[query]);if(error)return<div>Error:{error}</div>;if(loading)return<div>Loading...</div>;return(<ul>{results.map((item, index)=>(<likey={item.id??`item-${index}`}>{item.title??'(Untitled)'}</li>))}</ul>);}

    Note that the effect function itself is not async. An inner async function is declared and called immediately. This matters because useEffect expects its return value to be either undefined or a cleanup function. React ignores a returned promise and never registers a cleanup function from it.

    Reference Table: Setup, Cleanup, and Pitfalls at a Glance

    The following table captures the universal pattern across the most common side effects in React applications. Each row shows the same structure: acquire a re failure mode when cleanup is missing

    ScenarioSetup ActionCleanup ActionCommon Pitfall
    setInterval / setTimeoutStart timerclearInterval / clearTimeoutStacking intervals on dependency change
    addEventListenerAttach handlerremoveEventListener with same refInline arrow ≠ same reference
    fetchFire request with signalcontroller.abort()Stale response overwrites state
    WebSocketnew WebSocket(url) + attach handlersNull handlers, then ws.close()Connection leak on unmount
    Third-party subscription (e.g., Firebase onSnapshot)SubscribeCall unsubscribe functionForgetting the return value is the unsub

    WebSocket and Firebase patterns appear here for reference; see MDN’s WebSocket API docs and Firebase documentation for full implementation details.

    The pattern is always the same: acquire recreates something, cleanup destroys it. If setup connects, cleanup disconnects. Memorize this symmetry and every new side effect becomes a direct application of the same principle

    PR Review Checklist for useEffect Cleanup

    This checklist is designed to be dropped directly into a team’s contributing guidelines or used as a mental walkthrough during code review. Every question targets a specific category of cleanup bug.

    1. Does every useEffect that creates a side effect return a cleanup function?
    2. Does the cleanup fully reverse the setup (clear timer, remove listener, abort fetch)?
    3. Are event handler references stable (named function or useCallback) so removeEventListener actually matches?
    4. Is AbortError caught and handled separately from genuine network errors (e.g., returning early without setting error state or logging)?
    5. Does the dependency array include every value referenced inside the effect?
    6. Has the component been tested under React StrictMode double-mount?
    7. Are there any eslint-disable react-hooks/exhaustive-deps comments? If so, is each justified with an accompanying code comment explaining why the suppression is safe?

    A PR that passes all seven checks covers the most frequently reported categories of cleanup-related bugs before they reach production.

    Common Mistakes Even Intermediate Developers Make

    Suppressing the Exhaustive-Deps Lint Rule

    The react-hooks/exhaustive-deps ESLint rule exists to catch missing dependencies that cause stale closures. Disabling the rule hides the bug instead of fixing it. The effect closes over a stale value and never re-runs in response to that value changing, producing a stale closure that is difficult to reproduce and debug. When the rule flags a dependency, add it. If that causes unwanted re-runs, restructure the effect: extract logic, split into multiple effects, or stabilize references with useCallback or useMemo. Suppression should be a last resort, always paired with a comment explaining why the omission is intentional and safe.

    Returning a Promise Instead of a Function

    Marking the effect callback as async is a common trap. An async function implicitly returns a promise. React expects the return value to be either undefined or a synchronous cleanup function. React ignores a returned promise and never registers a cleanup function, so nothing runs on unmount or dependency change. The fix is always the same: define an inner async function inside the effect and invoke it immediately, keeping the outer effect function synchronous so that the return statement provides a proper cleanup function.

    React ignores a returned promise and never registers a cleanup function, so nothing runs on unmount or dependency change.

    Key Takeaways

    • Every useEffect that acquires a resource must return a cleanup function that releases it. The only exception: an effect that produces no persistent side effect (e.g., a one-time fire-and-forget analytics call).
    • AbortController is the modern, browser-native tool for cancelling fetch requests. It eliminates race conditions without boolean flags or sequence counters.
    • Acquire resource, release resource, declare dependencies honestly. That principle applies to timers, listeners, fetches, WebSockets, and third-party subscriptions alike.
    • Drop the seven-item PR review checklist into your contributing guidelines or pull request template. It catches the most commonly reported cleanup bugs before they reach production.

    Sharing our passion for building incredible internet things.

    Fetch listeners Races Timers
    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Tool Tech Team
    • Website

    Related Posts

    How to Source Passive Candidates with AI in 2026

    October 28, 2026

    7 Ways to Keep API Changes Safe as More Teams Join a Project

    October 28, 2026

    7 Checks That Make Form Bugs Easier to Reproduce Across Teams

    October 27, 2026

    Browser Fingerprinting Explained: How Websites Identify You and How Developers Can Reduce It

    October 27, 2026

    Best AI Recruiting Agents in 2026: 6 Compared

    October 26, 2026

    7 Engineering Patterns for Measuring Traffic From AI Search and Chat Tools

    October 26, 2026
    Leave A Reply Cancel Reply

    Top posts
    Web Hosting

    How to Source Passive Candidates with AI in 2026

    By Tool Tech Team
    Digital Marketing

    How to Teach Content Strategy to Stakeholders Who Don’t Speak Marketing

    By Tool Tech Team
    AI Tools

    Reddit is the latest platform to embrace the ‘For You’ feed

    By Tool Tech Team
    Editors Picks

    How to Source Passive Candidates with AI in 2026

    October 28, 2026

    How to Teach Content Strategy to Stakeholders Who Don’t Speak Marketing

    October 28, 2026

    Reddit is the latest platform to embrace the ‘For You’ feed

    October 28, 2026

    iOS 27.2 brings two new features to Apple’s CarPlay

    October 28, 2026
    About Us

    Welcome to ToolTechBlog, your trusted source for the latest insights, reviews, and practical guides on AI tools, business software, cybersecurity, web hosting, and consumer technology.
    Our mission is simple: to help individuals, entrepreneurs, freelancers, students, and businesses discover the right digital tools to improve productivity, streamline workflows, and make informed technology decisions.

    Our Picks

    How to Source Passive Candidates with AI in 2026

    October 28, 2026

    How to Teach Content Strategy to Stakeholders Who Don’t Speak Marketing

    October 28, 2026

    Reddit is the latest platform to embrace the ‘For You’ feed

    October 28, 2026
    Top Reviews

    The AI Hype Index: Unsexy AI

    July 29, 2026

    What it is and How to Fix it

    July 29, 2026

    LG to Ban Residential Proxies from Smart TV Apps

    July 29, 2026

    © 2026 tooltechblog.com. All rights reserved. Designed by DD.

    • About Us
    • Contact Us
    • Terms and Conditions
    • Privacy Policy
    • Disclaimer

    Type above and press Enter to search. Press Esc to cancel.