{"id":165,"date":"2026-10-19T04:56:23","date_gmt":"2026-10-19T04:56:23","guid":{"rendered":"https:\/\/webdevpuneet.com\/blog\/?p=165"},"modified":"2026-10-19T04:56:38","modified_gmt":"2026-10-19T04:56:38","slug":"useeffect-explained-8-common-mistakes-and-how-to-fix-them","status":"publish","type":"post","link":"https:\/\/webdevpuneet.com\/blog\/useeffect-explained-8-common-mistakes-and-how-to-fix-them\/","title":{"rendered":"useEffect Explained: 8 Common Mistakes and How to Fix Them"},"content":{"rendered":"<p><!--\n================================================================\n  POST TITLE (paste into the WordPress title field \u2014 it becomes the page H1)\n  useEffect Explained: 8 Common Mistakes and How to Fix Them\n\n  SEO TITLE (58 chars, target 50\u201360, max 60 \u2014 \"Optimize SEO\" panel \u2192 SEO TITLE)\n  useEffect Explained: 8 Common Mistakes and How to Fix Them\n\n  SEO DESCRIPTION (153 chars, target 150\u2013160, max 160 \u2014 \"Optimize SEO\" panel \u2192 SEO DESCRIPTION)\n  8 common React useEffect mistakes with live broken-vs-fixed demos: missing deps, stale closures, leaked intervals, object deps, race conditions and more.\n\n  HOW TO PASTE: click the first line after this comment, press Ctrl+Shift+End, Ctrl+C.\n  In WordPress click \"Type \/ to choose a block\", then Ctrl+V.\n  Fallback: \u22ee menu \u2192 Code editor \u2192 paste.\n================================================================\n--><\/p>\n\n\n<p class=\"wp-block-paragraph\">Almost every confusing <code>useEffect<\/code> bug comes from the same handful of misunderstandings, repeated in different clothes: treating the dependency array as optional, closing over a value that&#8217;s already gone stale, forgetting that objects are compared by reference, or reaching for an effect when the answer was a plain calculation or an event handler all along. None of these are edge cases \u2014 they&#8217;re the ones nearly every React developer hits in their first few months, and several of them ship into production because nothing crashes when they&#8217;re wrong; the UI just quietly does the incorrect thing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The 8 demos below are all live and interactive, and every one that has a &#8220;broken&#8221; side keeps it running right next to the fix so you can watch the actual difference in behavior, not just read about it. Every embed also has a <strong>Fork &amp; Edit<\/strong> button that opens the exact snippet, already running, in <a href=\"https:\/\/webdevpuneet.com\/ui-snippets\/mycode\/\">My Code<\/a> \u2014 webdevpuneet&#8217;s free in-browser editor. Change a dependency array, delete a cleanup function, see what breaks \u2014 that&#8217;s a faster way to build real intuition than any explanation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The one-sentence rule<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>useEffect(function () {\n  \/\/ runs after React commits this render to the screen\n  doSomething(value);\n\n  return function () {\n    \/\/ runs before the NEXT effect run, and once more on unmount\n    undoSomething();\n  };\n}, &#91;value]); \/\/ React re-runs the effect only when something in THIS array changed<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>useEffect<\/code> is a way to say &#8220;after the screen updates, synchronize something outside React with what just changed&#8221; \u2014 a subscription, a timer, the document title, a network request. The dependency array is not a performance knob you can skip; it&#8217;s the actual list of values the effect closes over, and it&#8217;s how React decides whether to re-run. Get it wrong in either direction \u2014 too few values, or a value that changes identity every render even though its contents don&#8217;t \u2014 and the effect runs at the wrong time, not just too often or too rarely. Every demo below is one specific way that goes wrong, and the one-line fix for it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When should you actually use useEffect?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Half of the mistakes below aren&#8217;t really about dependency arrays or cleanup functions \u2014 they&#8217;re about reaching for an effect when nothing outside React was involved in the first place. Before writing one, it&#8217;s worth checking which category the logic actually falls into.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Use an Effect for:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Synchronizing with an external system (a non-React widget, a map instance, a chart library)<\/li>\n\n\n\n<li>Subscriptions (WebSockets, event emitters, external stores)<\/li>\n\n\n\n<li>Timers (<code>setInterval<\/code>, <code>setTimeout<\/code> that needs to persist across renders)<\/li>\n\n\n\n<li>Browser APIs (<code>document.title<\/code>, focus management, <code>IntersectionObserver<\/code>)<\/li>\n\n\n\n<li>Network synchronization (fetching data that depends on props or state)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Don&#8217;t use an Effect for:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Calculating derived values (demo #6 below \u2014 just compute it during render)<\/li>\n\n\n\n<li>Handling user events (demo #7 below \u2014 put the logic in the handler that caused it)<\/li>\n\n\n\n<li>Transforming data for rendering (filtering, sorting, formatting \u2014 do it inline or with <code>useMemo<\/code>)<\/li>\n\n\n\n<li>Updating state simply because another state changed (a giveaway that the &#8220;derived&#8221; state should be a calculation instead)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The syntax of <code>useEffect<\/code> is genuinely small \u2014 a function and a dependency array. Most of the pain people associate with it actually comes from using it in the second list above, where no dependency array or cleanup function would have fixed the real problem: the effect shouldn&#8217;t have existed. Keep that distinction in mind as you go through the 8 demos \u2014 #1 through #5 are genuine effect mistakes (timing, closures, cleanup, race conditions), while #6, #7, and #8 are really about recognizing when an effect was the wrong tool to begin with.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. No Dependency Array Runs After Every Single Render<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Both counters share one unrelated &#8220;tick&#8221; button. Watch the effect-run count on each side as you click it.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/16\/1.html\" style=\"width:100%;height:480px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Missing Dependency Array Runs on Every Render Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>useEffect(function () {\n  \/\/ no array at all -- runs after EVERY render, no matter what changed\n});\n\nuseEffect(function () {\n  \/\/ empty array -- runs exactly once, after the first render\n}, &#91;]);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Leaving off the dependency array entirely doesn&#8217;t mean &#8220;run once&#8221; \u2014 it means React has no list to compare against, so it treats the effect as always needing to re-run. Clicking the unrelated tick button in this demo causes a render for a reason that has nothing to do with either effect, and the &#8220;no array&#8221; side still fires anyway, climbing every time. The empty-array side runs once, at mount, and never again, because comparing against an empty list never finds a difference. The visual proof here uses a ref rather than state to track the run count deliberately \u2014 calling <code>setState<\/code> unconditionally inside a no-array effect is itself a classic way to create an accidental infinite render loop, which is the same runaway loop demo #4 shows from a different angle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> understanding that &#8220;I forgot the array&#8221; and &#8220;I meant an empty array&#8221; are two completely different behaviors, not a stricter\/looser version of the same thing. <strong>Tip:<\/strong> if you genuinely need something to run after every render, that&#8217;s usually a sign the logic belongs somewhere else \u2014 a plain calculation during render, or the event handler that actually caused the change.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Stale Closure from a Missing Dependency<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click &#8220;+1&#8221; a few times, then click Start on each side. The broken one keeps adding 1 to a frozen old count; the fixed one reads the real current count.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/16\/2.html\" style=\"width:100%;height:480px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Stale Closure from a Missing Dependency Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>useEffect(function () {\n  var id = setInterval(function () {\n    setLog(count + 1);   \/\/ \"count\" is whatever it was WHEN THIS EFFECT RAN\n  }, 500);\n  return function () { clearInterval(id); };\n}, &#91;count]);   \/\/ include it, and a new interval is created with a fresh closure<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A JavaScript closure captures the actual variable binding available when the function was created \u2014 not a live, ever-updating reference. When <code>count<\/code> is missing from the dependency array, the interval callback is created once and keeps reading whatever <code>count<\/code> equaled at that exact moment, forever, even as the real value moves on. Including <code>count<\/code> in the array is what makes React tear down the old interval and create a new one \u2014 with a fresh closure over the current value \u2014 every time the count actually changes. In the demo, bump the count while both intervals are already running: the broken side keeps adding to the number it started with, while the fixed side always adds to whatever the count genuinely is right now.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> any effect that sets up a timer, subscription, or event listener that reads component state inside its callback \u2014 the dependency array is what keeps that callback from going stale. <strong>Tip:<\/strong> if a value legitimately shouldn&#8217;t trigger a re-subscription but the callback still needs its current value, a ref that&#8217;s updated on every render (not included in the array) is the usual escape hatch \u2014 that&#8217;s worth its own dedicated post.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Forgetting the Cleanup Function Leaks the Interval<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Mount a timer, let it tick a few times, then unmount it. Without a cleanup function, it keeps running invisibly and logging in the background.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/16\/3.html\" style=\"width:100%;height:480px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Forgetting the Cleanup Function Leaks the Interval Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>useEffect(function () {\n  var id = setInterval(tick, 500);\n  return function () { clearInterval(id); };   \/\/ &lt;-- easy to forget, never optional\n}, &#91;]);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">React doesn&#8217;t know what <code>setInterval<\/code>, <code>addEventListener<\/code>, or a subscription&#8217;s <code>.connect()<\/code> actually did \u2014 it only knows to call whatever function your effect returns, right before the next effect run and once more on unmount. Skip the <code>return<\/code> statement, and nothing ever tells the browser to stop that timer. In this demo, unmounting the component removes it from the screen entirely, but the log panel underneath \u2014 driven by the same leaked interval, writing to a plain variable outside React \u2014 keeps growing. The component is gone; the interval it created is not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> any effect that creates something with an explicit teardown API \u2014 timers, event listeners, subscriptions, observers, WebSocket connections. If the thing you created has a matching &#8220;stop&#8221; method, your effect needs a cleanup function that calls it. <strong>Tip:<\/strong> a missing cleanup function rarely throws an error or shows up in a quick test \u2014 it shows up as a slow memory leak or duplicate event handlers after a user navigates around your app for a few minutes, which is exactly what makes it worth checking for deliberately rather than waiting to notice.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. A New Object Every Render Breaks the Dependency Array<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Both effects depend on a filter object built the same way. Click Start on each and watch what happens to the run count.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/16\/4.html\" style=\"width:100%;height:480px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"A New Object Every Render Breaks the Dependency Array Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ BROKEN: a new object, every single render\nvar filters = { min: minPrice };\n\n\/\/ FIXED: the same object reference until minPrice actually changes\nvar filters = useMemo(function () {\n  return { min: minPrice };\n}, &#91;minPrice]);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">React compares dependency array entries with <code>Object.is<\/code> \u2014 essentially <code>===<\/code> \u2014 which checks identity, not contents. <code>{ min: 10 }<\/code> built on one render and <code>{ min: 10 }<\/code> built on the next are never <code>===<\/code> to each other, even though they look identical, because object literals create a brand-new reference every time they&#8217;re evaluated. If that object is a dependency and the effect also updates state, the result is a genuine infinite loop: render creates a new object, effect sees a &#8220;changed&#8221; dependency and runs, running updates state, updating state triggers a render, which creates another new object. This demo caps the broken side at 40 runs specifically so it can&#8217;t freeze your tab \u2014 in real, unguarded code, this exact shape is one of the most common causes of a page that locks up or spikes to 100% CPU. <code>useMemo<\/code> fixes it by returning the exact same object reference across renders until <code>minPrice<\/code> itself changes, which is the actual value the effect should care about.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> recognizing &#8220;Maximum update depth exceeded&#8221; or an unexplained CPU spike as a reference-identity problem first, before assuming the logic itself is wrong. <strong>Tip:<\/strong> this applies identically to arrays and to inline functions passed as dependencies \u2014 <code>useMemo<\/code> for computed values, <code>useCallback<\/code> for functions, same underlying reason. If your project runs the React Compiler, it memoizes values like this filter object for you; without it, <code>useMemo<\/code> is the manual version of the same fix.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Race Condition from a Missing Ignore Flag<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click &#8220;Ada&#8221; then immediately click &#8220;Bo&#8221; \u2014 Ada&#8217;s search is deliberately slower. Watch which name ends up on screen.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/16\/5.html\" style=\"width:100%;height:480px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Race Condition from a Missing Ignore Flag Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>useEffect(function () {\n  var ignore = false;\n  fetchResults(query).then(function (data) {\n    if (!ignore) setResults(data);   \/\/ check BEFORE applying a late response\n  });\n  return function () { ignore = true; };   \/\/ flips true the moment a newer request starts\n}, &#91;query]);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Nothing about <code>fetch<\/code> or a promise guarantees that requests resolve in the order they were sent. A fast, later request can finish before a slow, earlier one \u2014 and without anything to stop it, the earlier response still calls <code>setResults<\/code> when it eventually arrives, overwriting the correct, more recent answer with a stale one. The fix isn&#8217;t cancelling the actual network request \u2014 it&#8217;s a local <code>ignore<\/code> flag, scoped to that one specific effect run, that a fresh effect run&#8217;s cleanup function flips to <code>true<\/code> before applying its own response. In the demo, searching &#8220;Ada&#8221; and immediately searching &#8220;Bo&#8221; means Bo&#8217;s fast response arrives first \u2014 the broken panel still lets Ada&#8217;s slow response clobber it a moment later; the fixed panel&#8217;s stale response checks the flag and quietly no-ops.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> any effect that fetches data based on a value that can change again before the request finishes \u2014 search-as-you-type, tab switching, paginated data, anything driven by fast user input. <strong>Tip:<\/strong> if you&#8217;re using a data-fetching library (React Query, SWR, or React&#8217;s own <code>use<\/code> with Suspense), this exact problem is already solved for you internally \u2014 this pattern matters most when you&#8217;re calling <code>fetch<\/code> directly inside a plain <code>useEffect<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Derived State Doesn&#8217;t Need an Effect<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Type in the cart. Both totals end up correct \u2014 but watch the render counter while you type.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/16\/6.html\" style=\"width:100%;height:480px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"Derived State Doesn't Need an Effect Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Unnecessary: its own state, kept in sync with an effect\nuseEffect(function () { setTotal(qty * price); }, &#91;qty, price]);\n\n\/\/ Just calculate it -- no state, no effect, no extra render\nvar total = qty * price;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If a value can be computed directly from props or state already available during render, it doesn&#8217;t need to be state itself \u2014 storing it separately and syncing it with an effect means every change to the inputs causes two renders instead of one: the first with the stale, not-yet-recalculated value, and a second, effect-triggered one with the corrected value. Neither render is wrong forever, but the first one is genuinely wasted work the browser has to do and, briefly, paint. In this demo, both cart totals are numerically correct at rest \u2014 the difference only shows up in the render counter, which is exactly why this mistake is easy to ship without noticing: nothing about the final UI looks broken, it&#8217;s just quietly doing more work than it needs to.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> catching state that&#8217;s actually just a calculation wearing a <code>useState<\/code> costume \u2014 totals, filtered lists, formatted strings, anything fully determined by other values you already have. <strong>Tip:<\/strong> the giveaway is usually an effect whose entire body is a single <code>setSomething(...)<\/code> call with no external system involved \u2014 no timer, no subscription, no network call, nothing outside React. If that&#8217;s all it does, it&#8217;s very likely a candidate to become a plain variable instead.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. A Side Effect That Should Be an Event Handler<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Click &#8220;Load saved cart&#8221; on each side \u2014 it sets a quantity directly, the way restoring from storage would. No item was actually added, so no toast should appear.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/16\/7.html\" style=\"width:100%;height:480px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"A Side Effect That Should Be an Event Handler Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Reacts to a STATE CHANGE -- can't tell WHY qty changed\nuseEffect(function () {\n  if (qty &gt; 0) showToast('Item added');\n}, &#91;qty]);\n\n\/\/ Reacts to the ACTUAL EVENT -- knows exactly what just happened\nfunction handleAdd() {\n  setQty(qty + 1);\n  showToast('Item added');\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">An effect that watches a piece of state and reacts when it changes has no way to know <em>why<\/em> it changed \u2014 only that it did. If quantity can change for more than one reason (a user clicking &#8220;Add,&#8221; but also loading a saved cart, undoing an action, or syncing from a server), an effect keyed to quantity fires the same way for all of them, even the ones that shouldn&#8217;t trigger an &#8220;item added&#8221; toast at all. The fix is recognizing that &#8220;the user clicked Add&#8221; is an event you already have direct access to \u2014 it belongs in the click handler that causes it, not inferred after the fact from a state change. In the demo, loading a saved cart sets the same quantity state both panels track; the broken panel&#8217;s effect can&#8217;t distinguish that from a real add and shows the toast anyway, while the fixed panel&#8217;s load handler simply never calls the toast function.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> analytics events, toasts, confirmations, and any &#8220;when the user does X&#8221; logic \u2014 if you can name the user action that should trigger it, it almost always belongs in that action&#8217;s handler. <strong>Tip:<\/strong> this is one of the most common patterns in the official React docs&#8217; &#8220;You Might Not Need an Effect&#8221; guidance, and for good reason \u2014 it&#8217;s the single easiest category of unnecessary effect to actually remove once you&#8217;re looking for it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. One Effect, Two Unrelated Concerns<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Change the theme (unrelated to chat). Watch whether the chat connection below reconnects when it has no reason to.<\/p>\n\n\n\n<iframe src=\"https:\/\/webdevpuneet.com\/demos\/a1\/16\/8.html\" style=\"width:100%;height:480px;border:0;border-radius:10px;overflow:hidden;\" loading=\"lazy\" title=\"One Effect, Two Unrelated Concerns Demo by webdevpuneet.com\"><\/iframe>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The rule:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ One effect, two concerns -- theme changes force a chat reconnect\nuseEffect(function () {\n  connectToChat();\n  document.title = 'Theme: ' + theme;\n}, &#91;theme]);\n\n\/\/ Split by concern -- each effect has only the dependency it actually needs\nuseEffect(function () { connectToChat(); }, &#91;]);\nuseEffect(function () { document.title = 'Theme: ' + theme; }, &#91;theme]);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A dependency array has to honestly list every value the effect body reads \u2014 there&#8217;s no way to include a value for one line and exempt it for another. Once an unrelated concern shares an effect with something that does depend on a fast-changing value, that unrelated concern is forced to re-run on the same schedule, whether it needs to or not. In the demo, switching between light and dark theme has nothing to do with the chat connection, but the combined effect reconnects anyway because <code>theme<\/code> is in its dependency array. Splitting into two effects \u2014 one with an empty array for the connection, one with <code>[theme]<\/code> for the title \u2014 lets each concern re-run only on its own actual schedule.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Best for:<\/strong> any effect whose body you&#8217;d describe with the word &#8220;and&#8221; \u2014 connects to chat <em>and<\/em> updates the title, logs analytics <em>and<\/em> starts a timer. That &#8220;and&#8221; is usually the sign it should be two effects. <strong>Tip:<\/strong> this is also why <code>useEffect<\/code> can be called as many times as you want inside one component \u2014 there&#8217;s no cost to splitting by concern, and the dependency arrays end up smaller and more honest as a direct result.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common pitfalls<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Treating the dependency array as a performance setting instead of a correctness requirement.<\/strong> Demos #1 and #2 both come from the same root cause \u2014 the array isn&#8217;t optional tuning, it&#8217;s the actual list of values the effect&#8217;s closure depends on.<\/li>\n\n\n\n<li><strong>Assuming two objects with the same contents are the same dependency.<\/strong> Demo #4 is the direct proof \u2014 <code>{ min: 10 }<\/code> is never <code>===<\/code> to another <code>{ min: 10 }<\/code>, no matter how identical they look.<\/li>\n\n\n\n<li><strong>Forgetting that a promise can resolve out of order.<\/strong> Demo #5&#8217;s race condition exists because nothing about async code guarantees the request sent first finishes first.<\/li>\n\n\n\n<li><strong>Reaching for an effect before checking if it&#8217;s just a calculation or an event.<\/strong> Demos #6 and #7 are both &#8220;you might not need an effect&#8221; in disguise \u2014 one is derivable during render, the other already has a real event to hook into.<\/li>\n\n\n\n<li><strong>Cramming multiple concerns into one effect because they happen to run at the same time.<\/strong> Demo #8 shows the actual cost \u2014 an unrelated concern gets forced onto a schedule it never needed.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Why does my effect run twice in development?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In development, Strict Mode (React 18 and later) deliberately mounts, unmounts, and remounts every component once to surface exactly the bugs these demos cover \u2014 a missing cleanup function becomes obvious immediately, instead of only showing up later as a slow leak in production. It&#8217;s intentional and only happens in development; a correctly cleaned-up effect (like demo #3&#8217;s fixed version would be) handles the extra mount\/unmount cycle without any visible difference.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need to add every function I call inside an effect to the dependency array?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If the function is defined inside the component (and therefore recreated every render), yes, in principle \u2014 though a new function reference every render creates the exact same &#8220;always different&#8221; problem demo #4 shows with objects. The usual fixes are moving the function outside the component if it doesn&#8217;t need any props or state, or wrapping it in <code>useCallback<\/code> if it does, so its reference stays stable across renders that don&#8217;t actually change its behavior.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What&#8217;s the actual difference between useEffect and useLayoutEffect?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>useEffect<\/code> usually runs after the browser has painted the updated screen (React can run it earlier, before paint, when the update came from a click or keypress) \u2014 asynchronous relative to what the user sees. <code>useLayoutEffect<\/code> runs synchronously, after React updates the DOM but before the browser paints \u2014 useful for measuring layout (like an element&#8217;s size or position) and adjusting something before the user ever sees a flicker. Every demo in this post correctly uses <code>useEffect<\/code>; reach for <code>useLayoutEffect<\/code> only when you specifically need to prevent a visual flash caused by a DOM read-then-write.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I make useEffect&#8217;s callback itself async?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not directly \u2014 <code>useEffect<\/code> expects its callback to return either nothing or a cleanup function, and an <code>async<\/code> function always returns a promise instead. Demo #5 sidesteps this entirely by chaining <code>.then()<\/code> on the promise rather than using <code>await<\/code>, which keeps the effect callback itself synchronous. If you do want <code>await<\/code>-style syntax, the fix is the same idea one level deeper: define a separate <code>async<\/code> function inside the effect and call it immediately, keeping the outer effect function itself synchronous.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why does the linter warn about missing dependencies, and should I always listen to it?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>exhaustive-deps<\/code> ESLint rule is checking for exactly the stale-closure bug in demo #2 \u2014 it flags any value your effect reads that isn&#8217;t listed in the array. It&#8217;s right often enough that disabling it should be rare and deliberate, not a reflex; when it seems wrong, the more common resolution is restructuring the effect (as in several demos above) rather than silencing the warning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every demo above ships its full HTML, CSS and JS in the <strong>HTML \/ CSS \/ JS<\/strong> tabs in its own top bar. The fastest way to internalize this is to click <strong>Fork &amp; Edit<\/strong> on demo #4 \u2014 the infinite-loop one \u2014 and swap the <code>useMemo<\/code>&#8216;d object back for a plain object literal on the fixed side. Watch it immediately start climbing toward the same 40-run cap the broken side hits. That single experiment explains more than any diagram. Once you&#8217;ve got a version you like, save it to <a href=\"https:\/\/webdevpuneet.com\/ui-snippets\/mycode\/\">My Code<\/a> and it&#8217;s yours to keep, tweak further, or drop into a real project.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Recommended next step:<\/strong> open the <a href=\"https:\/\/webdevpuneet.com\/react-playground\/\">React Playground<\/a> \u2014 webdevpuneet&#8217;s full in-browser React sandbox \u2014 and rebuild demo #5&#8217;s race-condition fix from scratch against a search box of your own. Typing it out yourself, not just reading it, is what makes the pattern stick. For more tutorials like this one, browse the <a href=\"https:\/\/webdevpuneet.com\/blog\/category\/react\/\">React<\/a> category on the blog.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Almost every confusing useEffect bug comes from the same handful of misunderstandings, repeated in different clothes: treating the dependency array as optional, closing over a value that&#8217;s already gone stale, forgetting that objects are compared by reference, or reaching for an effect when the answer was a plain calculation or an event handler all along. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":167,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_et_pb_use_builder":"","_et_pb_old_content":"","_et_gb_content_width":"","advanced_seo_description":"8 common React useEffect mistakes with live broken-vs-fixed demos: missing deps, stale closures, leaked intervals, object deps, race conditions and more.","jetpack_seo_html_title":"useEffect Explained: 8 Common Mistakes and How to Fix Them","jetpack_seo_noindex":false,"jetpack_seo_schema_type":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"Struggling with React's `useEffect`? You're not alone.\n\nMany React bugs come from misunderstanding how effects, dependencies, and cleanup actually work.\n\nIn this guide, you'll learn how to avoid 8 common `useEffect` mistakes, including:\n\n1. Missing dependencies\n2. Infinite re-render loops\n3. Stale closures\n4. Memory leaks from intervals\n5. Unnecessary object dependencies\n6. Race conditions in API calls\n7. Incorrect cleanup functions\n8. Using `useEffect` when you don't need it\n\nEach mistake includes a broken-vs-fixed example with practical explanations to help you understand what's going wrong and how to fix it.\n\nWhether you're a React beginner or an experienced developer, these examples will help you write cleaner, more predictable React code.\n\nRead the full guide:\n\n#ReactJS #JavaScript #WebDevelopment #FrontendDevelopment #CodingTips #ReactHooks\n","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[8,9],"tags":[],"class_list":["post-165","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-react","category-tutorials","et-has-post-format-content","et_post_format-et-post-format-standard"],"jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/phrh5V-2F","jetpack_featured_media_url":"https:\/\/i0.wp.com\/webdevpuneet.com\/blog\/wp-content\/uploads\/2026\/10\/react-useeffect-explained-header-1200x630-1.jpg?fit=1200%2C630&ssl=1","_links":{"self":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/165","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/comments?post=165"}],"version-history":[{"count":1,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/165\/revisions"}],"predecessor-version":[{"id":168,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/posts\/165\/revisions\/168"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/media\/167"}],"wp:attachment":[{"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/media?parent=165"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/categories?post=165"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/webdevpuneet.com\/blog\/wp-json\/wp\/v2\/tags?post=165"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}