<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Parsa Jiravand</title>
    <description>The latest articles on DEV Community by Parsa Jiravand (@parsajiravand).</description>
    <link>https://dev.to/parsajiravand</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3831018%2Ff09b70fc-3b0d-4ce2-bb7e-d78ee6f7d701.jpg</url>
      <title>DEV Community: Parsa Jiravand</title>
      <link>https://dev.to/parsajiravand</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/parsajiravand"/>
    <language>en</language>
    <item>
      <title>Your Recipe App Is Hiding a Silent Video</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Sun, 06 Sep 2026 10:37:22 +0000</pubDate>
      <link>https://dev.to/parsajiravand/your-recipe-app-is-hiding-a-silent-video-21ma</link>
      <guid>https://dev.to/parsajiravand/your-recipe-app-is-hiding-a-silent-video-21ma</guid>
      <description>&lt;p&gt;Open dev tools on almost any recipe site mid-scroll and check the Elements panel. Buried near the bottom, there's a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tag. It's one pixel. It's muted. It's on an infinite loop. And if you actually watch what it's playing — nothing. It's a single frame of black, looping forever, going nowhere.&lt;/p&gt;

&lt;p&gt;That's not debug cruft someone forgot to delete. It's load-bearing. Rip it out and the page's actual job — keeping your phone's screen on while your hands are covered in flour — quietly breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem it's solving
&lt;/h2&gt;

&lt;p&gt;Mobile browsers don't give a web page a button that says "don't lock the screen." There's no &lt;code&gt;document.staySharp()&lt;/code&gt;. The OS owns that timer, and for a long time the only way JavaScript could influence it was indirectly — by doing something the OS already treats as a signal that the user is still engaged.&lt;/p&gt;

&lt;p&gt;Actively playing video is one of those signals. Every mobile OS defers the screen-lock timer while media is playing, because pausing a movie mid-scene to lock the screen would be a terrible experience. So people found the seam: play &lt;em&gt;something&lt;/em&gt;, even if that something is deliberately nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"keep-awake"&lt;/span&gt; &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;playsinline&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"silent-1px.mp4"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keep-awake&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;muted&lt;/code&gt; and &lt;code&gt;playsinline&lt;/code&gt; are load-bearing too — without them, most mobile browsers either block the autoplay outright or hijack the screen with a fullscreen player. Libraries like NoSleep.js packaged exactly this pattern for years, because it worked, and because there wasn't anything better to reach for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it falls apart
&lt;/h2&gt;

&lt;p&gt;It's a real technique and it did the job — but borrowing "video playback" as a proxy for "keep the screen awake" comes with everything you'd expect from repurposing the wrong primitive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autoplay policies fight you.&lt;/strong&gt; Some mobile browsers still block autoplaying video unless it happens inside a user gesture, so the trick can silently fail on exactly the browser version where you need it most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs a decoder, not a flag.&lt;/strong&gt; A one-pixel video is small, but "small" isn't "free" — you're spinning up video decoding hardware and a render loop to communicate a boolean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing tells you it stopped working.&lt;/strong&gt; If the tab is backgrounded in a way the browser doesn't treat as "still playing," the video pauses and your screen locks — with no error, no event, nothing to catch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's semantically a lie.&lt;/strong&gt; Anyone reading that markup six months from now has to reverse-engineer &lt;em&gt;why&lt;/em&gt; there's a phantom video before they can safely touch it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workaround earned its keep because the platform had a real gap. In 2019, Chrome closed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API that says what it means
&lt;/h2&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/screen-wake-lock-api-keep-screen-on/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Screen Wake Lock API does exactly one thing, and its name doesn't lie about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;wakeLock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestWakeLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;wakeLock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wakeLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;screen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Screen will stay on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fails if the document isn't visible, isn't a secure context,&lt;/span&gt;
    &lt;span class="c1"&gt;// or the platform refuses for its own reasons.&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;navigator.wakeLock.request("screen")&lt;/code&gt; — &lt;code&gt;"screen"&lt;/code&gt; is currently the only lock type the spec defines — returns a promise for a &lt;code&gt;WakeLockSentinel&lt;/code&gt;. While that sentinel is alive, the screen won't dim or lock on its own. No &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, no decoder, no lying markup. It needs a secure context (HTTPS or localhost) and the document has to actually be visible when you call it, or the promise rejects with a &lt;code&gt;NotAllowedError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Releasing it is just as direct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;releaseWakeLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;wakeLock&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;wakeLock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would be the whole post, except the API has one behavior that catches almost everyone the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part the happy path hides
&lt;/h2&gt;

&lt;p&gt;Switch away from the tab holding the lock — another window, another app, the phone's lock button — and the browser releases the wake lock for you. Silently. No exception. No rejected promise. Your &lt;code&gt;wakeLock&lt;/code&gt; variable still points at a sentinel object; it's just that the sentinel's job is now over, and nothing forces you to notice.&lt;/p&gt;

&lt;p&gt;Come back to the tab, and the screen goes right back to locking on schedule — because as far as the platform's concerned, you never asked it not to. If your only test was "click the button once, watch the screen stay on for thirty seconds," you'll ship this and find out from a support ticket instead.&lt;/p&gt;

&lt;p&gt;The sentinel does expose a &lt;code&gt;release&lt;/code&gt; event, and MDN's own guide handles the whole thing with a &lt;code&gt;visibilitychange&lt;/code&gt; listener that re-requests the lock the moment the tab becomes visible again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wakeLock&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visibilityState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requestWakeLock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One wrinkle worth planning for yourself: that condition only checks "did we ever request a lock," not "does the user still want one." If you also let people manually turn the feature off, null out &lt;code&gt;wakeLock&lt;/code&gt; on that path too — otherwise switching tabs and back will quietly turn a lock the user explicitly released back on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship it without breaking the browsers that don't have it
&lt;/h2&gt;

&lt;p&gt;Feature-detect before you touch any of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wakeLock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;requestWakeLock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Fall back to whatever you were doing before, or do nothing —&lt;/span&gt;
  &lt;span class="c1"&gt;// a screen that locks isn't a crash.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Support is solid across Chrome, Edge, and Opera, and Safari added it in 16.4. Firefox has historically lagged on this one — check caniuse.com for the current state before you rely on it, and always keep the feature-detect branch rather than assuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-pixel video, decoded
&lt;/h2&gt;

&lt;p&gt;Next time a cooking site keeps your screen alive while you're up to your elbows in dough, you'll know what's happening under the hood — and whether it's a phantom &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tag or three lines calling an API built for exactly that job.&lt;/p&gt;

&lt;p&gt;If you're still shipping the video trick somewhere, what's holding you back from swapping it — browser support, or just not knowing the replacement existed?&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/screen-wake-lock-api-keep-screen-on/quiz" rel="noopener noreferrer"&gt;Take the 7-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>browser</category>
      <category>api</category>
    </item>
    <item>
      <title>React Compiler 1.0: What useMemo You Can Delete</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Sat, 05 Sep 2026 10:17:49 +0000</pubDate>
      <link>https://dev.to/parsajiravand/react-compiler-10-what-usememo-you-can-delete-hgm</link>
      <guid>https://dev.to/parsajiravand/react-compiler-10-what-usememo-you-can-delete-hgm</guid>
      <description>&lt;p&gt;Open any React codebase built before late 2025 and you'll find the same defensive scaffolding in nearly every component: a &lt;code&gt;memo()&lt;/code&gt; wrapper here, a &lt;code&gt;useCallback&lt;/code&gt; there, a &lt;code&gt;useMemo&lt;/code&gt; around a sort you were never quite sure was expensive enough to justify it. Most of that code was never a response to a measured problem — it was insurance against a re-render you were &lt;em&gt;guessing&lt;/em&gt; might happen.&lt;/p&gt;

&lt;p&gt;As of &lt;strong&gt;React Compiler 1.0&lt;/strong&gt;, stable since October 2025, that guessing game is mostly over. The compiler does the same analysis you were doing by hand, applies it more precisely than hooks alone can, and in a few cases does things manual memoization structurally cannot do at all. This is episode two of &lt;strong&gt;React Deep Dive&lt;/strong&gt;, and it's about what "automatic memoization" actually means, what it removes from your code, and what it deliberately leaves for you to keep deciding.&lt;/p&gt;

&lt;p&gt;This article is written against &lt;strong&gt;React 19.2&lt;/strong&gt; (verified 19.2.8, the current npm release as of this writing) and &lt;strong&gt;React Compiler 1.0&lt;/strong&gt; (the stable &lt;code&gt;babel-plugin-react-compiler@1.0.0&lt;/code&gt; release, shipped October 7, 2025 at React Conf — verified against React's own release notes). If you want the re-render vocabulary this article leans on, &lt;a href="https://dev.to/parsajiravand/react-re-render-vs-remount-what-actually-triggers-each-5fok"&gt;episode one covered re-render vs. remount&lt;/a&gt; — useful background, not required reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll learn
&lt;/h2&gt;

&lt;p&gt;By the end of this article you'll be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explain &lt;em&gt;why&lt;/em&gt; React re-renders a whole subtree by default, and what memoization actually buys you when you add it&lt;/li&gt;
&lt;li&gt;Read code with &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; and know exactly what problem each one was solving&lt;/li&gt;
&lt;li&gt;Describe what React Compiler automates, including two cases manual hooks can't solve at all&lt;/li&gt;
&lt;li&gt;Know when you still need &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; even with the compiler installed&lt;/li&gt;
&lt;li&gt;Add the compiler to a project and read its lint diagnostics when it can't safely optimize something&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;You've written function components and used &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, and at least one of &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt;/&lt;code&gt;React.memo&lt;/code&gt; before, even if you couldn't fully explain why. No compiler internals or build-tooling experience required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The problem: manual memoization doesn't scale&lt;/li&gt;
&lt;li&gt;The mental model: what memoization actually buys you&lt;/li&gt;
&lt;li&gt;Stage 1: the re-render, without memoization&lt;/li&gt;
&lt;li&gt;Stage 2: the manual fix, and its subtle crack&lt;/li&gt;
&lt;li&gt;Stage 3: the same code, compiled&lt;/li&gt;
&lt;li&gt;Stage 4: turning it on&lt;/li&gt;
&lt;li&gt;Edge cases and gotchas&lt;/li&gt;
&lt;li&gt;Best practices&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;Cheat sheet&lt;/li&gt;
&lt;li&gt;Key takeaways&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem: manual memoization doesn't scale
&lt;/h2&gt;

&lt;p&gt;Here's a dashboard with a search box and a team roster underneath it. Nothing exotic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TeamRoster&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onInvite&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sendInvite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TeamRoster&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onInvite&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sortByActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// recomputed on every call&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MemberCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;member&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onInvite&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onInvite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;members&lt;/code&gt; never changes while you type. But every keystroke updates &lt;code&gt;query&lt;/code&gt;, which re-renders &lt;code&gt;Dashboard&lt;/code&gt;, which re-renders &lt;code&gt;TeamRoster&lt;/code&gt; — by default, with no memoization applied anywhere, React re-renders a component and everything below it whenever the component's own state or a parent's state changes. &lt;code&gt;TeamRoster&lt;/code&gt; re-runs &lt;code&gt;sortByActivity&lt;/code&gt; from scratch, builds a brand-new array, and hands every &lt;code&gt;MemberCard&lt;/code&gt; a brand-new &lt;code&gt;onInvite&lt;/code&gt; closure. Every card re-renders, on every keystroke, for a value that didn't change.&lt;/p&gt;

&lt;p&gt;The textbook fix is to memoize the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TeamRoster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TeamRoster&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onInvite&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sortByActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleInvite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onInvite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onInvite&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MemberCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;member&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onInvite&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleInvite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps — &lt;code&gt;sortByActivity&lt;/code&gt; only re-runs when &lt;code&gt;members&lt;/code&gt; actually changes. But look closely at the line that renders each card: &lt;code&gt;onInvite={() =&amp;gt; handleInvite(m.id)}&lt;/code&gt;. That inline arrow function is created fresh on every render, &lt;code&gt;useCallback&lt;/code&gt; wrapper or not, so &lt;code&gt;MemberCard&lt;/code&gt; still gets a new &lt;code&gt;onInvite&lt;/code&gt; prop every time and still re-renders. &lt;code&gt;useCallback&lt;/code&gt; cannot fix this without restructuring the code — hooks can only stabilize &lt;em&gt;values&lt;/em&gt;, and the value here is the arrow function &lt;em&gt;around&lt;/em&gt; &lt;code&gt;handleInvite&lt;/code&gt;, not &lt;code&gt;handleInvite&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;This is the actual shape of the problem: correct manual memoization requires you to trace every value that flows into every child, on every edit, forever. Miss one spot — and the spot above is easy to miss — and the memoization you added silently does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: what memoization actually buys you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The mental model:&lt;/strong&gt; React re-renders a component whenever its own state changes &lt;em&gt;and&lt;/em&gt; whenever its parent re-renders — regardless of whether the props it receives actually changed. Memoization doesn't stop a component from re-rendering because of its own state; it gives React a way to tell that a &lt;em&gt;child's&lt;/em&gt; inputs didn't change, so React's reconciler can skip that child's subtree entirely rather than re-run it and diff the result.&lt;/p&gt;

&lt;p&gt;Concretely: if a component returns the exact same element reference on two consecutive renders (not just equal-looking JSX, the same object in memory), React bails out of that subtree without touching it. &lt;code&gt;React.memo&lt;/code&gt; gets you this by comparing props before re-rendering the child; &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; get you this by keeping the &lt;em&gt;values passed into&lt;/em&gt; JSX stable, so the JSX built from them stays stable too.&lt;/p&gt;

&lt;p&gt;React Compiler's entire job is producing that same stability automatically, everywhere it's provably safe to do so — by reading your component's code and figuring out, per value, whether it could have changed since the last render. It doesn't change &lt;em&gt;when&lt;/em&gt; your component's own state causes it to re-render. It changes whether that re-render cascades into components that had nothing to do with the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: the re-render, without memoization
&lt;/h2&gt;

&lt;p&gt;Run the &lt;code&gt;Dashboard&lt;/code&gt;/&lt;code&gt;TeamRoster&lt;/code&gt; code above and every &lt;code&gt;MemberCard&lt;/code&gt; logs a render on every keystroke — you can watch this happen for real in the playground below, which runs the actual React 19.2.8 runtime, not a simulation. This is React doing exactly what it's documented to do: no memoization means no bailout, so the whole subtree re-runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; an unmemoized re-render isn't a bug. It's React's default, and it's usually fine — React is fast enough that most re-renders never cost anything a user would notice. The problem only shows up when a subtree is expensive enough, or large enough, that redoing it on every keystroke becomes visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/react-weekly-compiler-memoization/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: the manual fix, and its subtle crack
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;memo&lt;/code&gt;/&lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; version above fixes the expensive sort but not the inline arrow, for a structural reason worth sitting with: hooks can only be called unconditionally, at the top level of a component, so you can't &lt;code&gt;useCallback&lt;/code&gt; a function that's constructed &lt;em&gt;inside&lt;/em&gt; a &lt;code&gt;.map()&lt;/code&gt; callback per item — you'd be calling a hook in a loop, which breaks the Rules of Hooks. The only fix within hooks alone is to restructure the code, usually by pushing the click handler down into &lt;code&gt;MemberCard&lt;/code&gt; and passing just the id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; manual memoization isn't just tedious, it has real structural gaps — situations no combination of &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt;/&lt;code&gt;React.memo&lt;/code&gt; can close without changing how the code is written. That's the opening React Compiler was built to close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: the same code, compiled
&lt;/h2&gt;

&lt;p&gt;With React Compiler enabled, you write the &lt;em&gt;first&lt;/em&gt; version of &lt;code&gt;TeamRoster&lt;/code&gt; — no &lt;code&gt;memo&lt;/code&gt;, no &lt;code&gt;useMemo&lt;/code&gt;, no &lt;code&gt;useCallback&lt;/code&gt;, the inline arrow left exactly where it reads most naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TeamRoster&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onInvite&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sortByActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MemberCard&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;member&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onInvite&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onInvite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler analyzes the function body at build time and rewrites it — roughly, generating memoized slots for &lt;code&gt;sorted&lt;/code&gt; and for each card's element, comparing them against the previous render's values, and reusing the old result when nothing relevant changed. According to React's own compiler documentation, this handles the inline-arrow case correctly &lt;em&gt;with or without&lt;/em&gt; the arrow function, because the compiler is reasoning about the whole function's data flow, not applying a hook to a single named value.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This part is a &lt;strong&gt;model&lt;/strong&gt;, not a live demo: React Compiler is a build-time Babel transform, so it cannot run inside a static HTML page without a bundler. The playground above shows the &lt;em&gt;real, measurable effect&lt;/em&gt; — fewer re-renders — that flipping "apply manual memoization" produces with the actual React runtime; the compiled output is the same effect, generated for you, from the plain code shown here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things the compiler can do that manual hooks structurally can't, both documented in its own release notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memoize after an early return.&lt;/strong&gt; &lt;code&gt;useMemo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt; can't appear after a conditional &lt;code&gt;return&lt;/code&gt; — that's the Rules of Hooks. The compiler isn't a hook, so it can memoize values computed after one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle the inline-arrow case above&lt;/strong&gt; without you restructuring anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stage 4: turning it on
&lt;/h2&gt;

&lt;p&gt;Installing it is a dev dependency plus a build-tool integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; &lt;span class="nt"&gt;--save-exact&lt;/span&gt; babel-plugin-react-compiler@latest
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; eslint-plugin-react-hooks@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;eslint-plugin-react-hooks&lt;/code&gt;'s &lt;code&gt;recommended&lt;/code&gt; and &lt;code&gt;recommended-latest&lt;/code&gt; presets now ship the compiler's lint rules directly — this replaced the separate &lt;code&gt;eslint-plugin-react-compiler&lt;/code&gt; package when the compiler went stable, and the lint rules work even in projects that haven't added the compiler itself yet, because they're really flagging Rules-of-React violations.&lt;/p&gt;

&lt;p&gt;New projects scaffolded with recent versions of Vite, Next.js (15.3.1+), or Expo (SDK 54+) can start with the compiler already wired in. Existing codebases adopt it incrementally: point the compiler at one directory or route first, watch the lint output, and expand from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It only compiles components and hooks — not arbitrary functions.&lt;/strong&gt; A plain helper function called from inside a component gets memoized as a &lt;em&gt;call&lt;/em&gt;, but if that same expensive helper is called from three different components, each one still pays the cost independently; the compiler's memoization isn't shared across components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It requires the Rules of React.&lt;/strong&gt; The compiler assumes your components and hooks are pure — idempotent given the same props/state, no mutating props or state during render, no side effects in render. Code that breaks these rules in ways the compiler can statically detect gets flagged (surfaced through &lt;code&gt;eslint-plugin-react-hooks&lt;/code&gt;, with rules like &lt;code&gt;set-state-in-render&lt;/code&gt; and &lt;code&gt;set-state-in-effect&lt;/code&gt;); code that breaks them in ways JavaScript can't statically catch may compile without warning and behave subtly differently than before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removing existing manual memoization isn't automatically safe.&lt;/strong&gt; React's own guidance is to leave existing &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; calls in place, or test carefully before deleting them — the compiler's memoization boundaries won't always land in exactly the same places yours did, and if some effect elsewhere depends on one of your values staying referentially stable across specific renders, changing that boundary can change how often that effect fires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React 17 and 18 are supported, not just 19&lt;/strong&gt; — with a &lt;code&gt;target&lt;/code&gt; config option and the &lt;code&gt;react-compiler-runtime&lt;/code&gt; package as an added dependency. On React 19 neither is needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a build-time transform, full stop.&lt;/strong&gt; There's no runtime flag or devtools toggle; if it isn't wired into your bundler's config, none of this applies to your app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New code: stop hand-memoizing by default.&lt;/strong&gt; Write the plain version. Reach for &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; only when you need explicit control over a value's identity — most commonly, when that value is a dependency of an effect and you need to guarantee it won't cause the effect to over-fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn on the compiler-powered lint rules even before you install the compiler.&lt;/strong&gt; They're Rules-of-React checks, and they catch real bugs (state updates during render, unsafe ref reads) independent of whether you've adopted the compiler yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adopt incrementally in an existing codebase.&lt;/strong&gt; Compile one route or directory, watch for lint diagnostics and behavior regressions, then widen the scope. Pin the compiler to an exact version (&lt;code&gt;--save-exact&lt;/code&gt;) rather than a semver range if your test coverage is thin, since future versions may change memoization boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't reach for the compiler to fix a slow function.&lt;/strong&gt; If &lt;code&gt;sortByActivity&lt;/code&gt; above were genuinely expensive, calling it from several components would still re-run it in each one — profile first, and consider your own caching if the same expensive call is duplicated across the tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;"use no memo"&lt;/code&gt; as a scalpel, not a habit.&lt;/strong&gt; It opts one function out of compilation, useful while debugging a compiler diagnostic or isolating code the compiler can't yet handle — not a default you sprinkle everywhere "to be safe."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does React Compiler replace useEffect?
&lt;/h3&gt;

&lt;p&gt;No. The compiler is entirely about memoizing render-time values and JSX; effects are how you synchronize with something outside React, and the compiler has no opinion on what belongs in one or when it should run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I still need React.memo, useMemo, or useCallback with the compiler installed?
&lt;/h3&gt;

&lt;p&gt;Not by default — the compiler applies equivalent memoization automatically in most cases. They remain available as an explicit escape hatch, most notably when a value is used as an effect's dependency and you need to guarantee its identity stays stable on purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to delete all my existing useMemo/useCallback calls right now?
&lt;/h3&gt;

&lt;p&gt;Not automatically. React's own release guidance is to leave existing memoization in place, or remove it only after careful testing, because the compiler's generated memoization can land on slightly different boundaries than yours did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does React Compiler work with React 18 or older?
&lt;/h3&gt;

&lt;p&gt;Yes — it supports React 17 and up. Below React 19 you add a &lt;code&gt;target&lt;/code&gt; in the compiler config and depend on &lt;code&gt;react-compiler-runtime&lt;/code&gt;; on React 19 that extra dependency isn't needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if my component breaks the Rules of React?
&lt;/h3&gt;

&lt;p&gt;The compiler's validation passes encode the Rules of React and surface violations as diagnostics through &lt;code&gt;eslint-plugin-react-hooks&lt;/code&gt;. Statically detectable violations get flagged rather than silently miscompiled; violations JavaScript can't detect at compile time are the reason React recommends good test coverage before relying on the compiler in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I stop the compiler from touching one specific component?
&lt;/h3&gt;

&lt;p&gt;Add the &lt;code&gt;"use no memo"&lt;/code&gt; directive as the first line of that function's body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Before (manual)&lt;/th&gt;
&lt;th&gt;With React Compiler&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Skip re-rendering a child when unrelated state changes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;React.memo(Child)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Compiler keeps the child's JSX reference stable so React's reconciler bails out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stabilize a callback passed to a memoized child&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useCallback(fn, [deps])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Handles inline arrows written straight in JSX, which manual hooks can't&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avoid recomputing an expensive render-time value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useMemo(() =&amp;gt; calc(x), [x])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Only for values computed inside a component or hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memoize a value defined after an early return&lt;/td&gt;
&lt;td&gt;Not possible — breaks Rules of Hooks&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;One of the compiler's documented advantages over hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guarantee a value's identity for an effect dependency&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Still &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Documented escape hatch — keep using it here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opt one function out of compilation&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"use no memo"&lt;/code&gt; directive&lt;/td&gt;
&lt;td&gt;For debugging or code incompatible with the compiler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enable Rules-of-React lint checks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eslint-plugin-react-compiler&lt;/code&gt; (superseded)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eslint-plugin-react-hooks@latest&lt;/code&gt;, &lt;code&gt;recommended&lt;/code&gt; preset&lt;/td&gt;
&lt;td&gt;Ships the compiler's lint rules directly since 1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run on React 17/18&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;react-compiler-runtime&lt;/code&gt; + &lt;code&gt;target&lt;/code&gt; config&lt;/td&gt;
&lt;td&gt;React 19 needs neither&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Minimal install for a React 19 project&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; &lt;span class="nt"&gt;--save-exact&lt;/span&gt; babel-plugin-react-compiler@latest
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; eslint-plugin-react-hooks@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/react-weekly-compiler-memoization/quiz" rel="noopener noreferrer"&gt;Take the 8-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React re-renders a component's whole subtree by default whenever its state changes; memoization's actual job is giving children a stable reference so React's reconciler can bail out of re-rendering them.&lt;/li&gt;
&lt;li&gt;React Compiler 1.0, stable since October 2025, automates that memoization at build time — including cases hooks structurally cannot solve, like an inline arrow written in JSX or a value defined after an early return.&lt;/li&gt;
&lt;li&gt;It only compiles components and hooks that hold to the Rules of React, and it only memoizes work inside them — not arbitrary functions shared across components.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; aren't obsolete. Keep them where a value's referential identity is a correctness requirement, like an effect dependency, and don't strip existing memoization from old code without testing first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pile of &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt;/&lt;code&gt;memo()&lt;/code&gt; wrappers from the opening paragraph isn't gone because you finally found time to delete it by hand — it's gone because, as of React 19.2 with the compiler enabled, you stop needing to write most of it in the first place. The insurance policy against re-renders you were never sure would happen is now something the build step carries for you.&lt;/p&gt;

&lt;p&gt;Have you turned the compiler on in a real codebase yet — did the lint rules catch anything you didn't expect? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Timer Doesn't Know You Tabbed Away</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Sat, 05 Sep 2026 10:17:18 +0000</pubDate>
      <link>https://dev.to/parsajiravand/your-timer-doesnt-know-you-tabbed-away-3cjp</link>
      <guid>https://dev.to/parsajiravand/your-timer-doesnt-know-you-tabbed-away-3cjp</guid>
      <description>&lt;p&gt;Picture a dashboard tile polling an endpoint every two seconds to keep a number fresh. You switch to Slack to answer a message. Four minutes later you're still there, and the tile is still polling — a request every two seconds, into a tab nobody has looked at since you left it.&lt;/p&gt;

&lt;p&gt;Multiply that by however many tabs your users leave open, and it's not a rounding error. It's server load and battery drain for work that produces zero value, because the one piece of information that would have stopped it — &lt;em&gt;is anyone actually looking at this&lt;/em&gt; — was never checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that feels obvious
&lt;/h2&gt;

&lt;p&gt;The instinct is to pause on &lt;code&gt;blur&lt;/code&gt; and resume on &lt;code&gt;focus&lt;/code&gt;. It reads clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blur&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pollId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;focus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pollId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;startPolling&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship that, and eventually someone files a strange bug: a video that's supposed to keep playing in the background pauses when they click into DevTools. Or a live ticker that stops updating the instant they open a browser extension's popup. Or — the one that's easy to miss because it only happens on multi-monitor setups — a dashboard that "goes idle" while it's sitting fully visible on a second screen, because the user is typing in a different app on their main monitor.&lt;/p&gt;

&lt;p&gt;None of those tabs are hidden. The content is right there on the glass. &lt;code&gt;blur&lt;/code&gt; fired anyway, because &lt;code&gt;blur&lt;/code&gt; and &lt;code&gt;focus&lt;/code&gt; answer a narrower question than the one you actually asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question you meant to ask
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;blur&lt;/code&gt;/&lt;code&gt;focus&lt;/code&gt; tell you whether a window has &lt;em&gt;keyboard focus&lt;/em&gt;. That's genuinely useful for things like "should this input show its focus ring" — it is not the same as "can a human currently see this content." A window can lose focus while staying fully rendered and visible; that's exactly the second-monitor case above, and it's why MDN's own guidance singles this out when explaining why the Page Visibility API exists at all, rather than just reusing focus events.&lt;/p&gt;

&lt;p&gt;The API that answers the real question is small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;document.visibilityState&lt;/code&gt; — &lt;code&gt;"visible"&lt;/code&gt; or &lt;code&gt;"hidden"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;document.hidden&lt;/code&gt; — the older boolean shorthand for the same thing.&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;visibilitychange&lt;/code&gt; event, fired on &lt;code&gt;document&lt;/code&gt; whenever that state flips.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It goes "hidden" when the tab is switched away from, the window is minimized, or (on most platforms) the screen locks — the set of conditions where the content is, as far as the browser can tell, definitely not on anyone's retina. It stays &lt;code&gt;"visible"&lt;/code&gt; in the second-monitor case above, because it correctly is. This has been standard and unprefixed in every evergreen browser for well over a decade — there's no feature-detection dance required.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/page-visibility-api-tab-aware-timers/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it up properly
&lt;/h2&gt;

&lt;p&gt;Swap the listener, not just the vocabulary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pollId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startPolling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pollId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchLatest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopPolling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pollId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;pollId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stopPolling&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pollId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;startPolling&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;startPolling&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole change. But there's a second gotcha waiting even after you've picked the right event: don't trust the &lt;em&gt;tick count&lt;/em&gt; to tell you how long the tab was gone.&lt;/p&gt;

&lt;p&gt;Browsers throttle timers running in tabs nobody can see — it's a deliberate battery-saving move, and it means a &lt;code&gt;setInterval&lt;/code&gt; you started before the tab was hidden won't necessarily fire on schedule while it's backgrounded. If your "time away" logic counts ticks (&lt;code&gt;ticksElapsed * intervalMs&lt;/code&gt;), a long background stretch will under-report itself, because some of those ticks simply never fired.&lt;/p&gt;

&lt;p&gt;The fix is to stop counting ticks and start reading the clock. Stamp &lt;code&gt;Date.now()&lt;/code&gt; on the way out and the way back in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hiddenAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hiddenAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hiddenAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hiddenMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;hiddenAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hiddenMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// catch up state using real elapsed time&lt;/span&gt;
    &lt;span class="nx"&gt;hiddenAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;visibilitychange&lt;/code&gt; gives you the exact moments; wall-clock math gives you the exact duration between them. Neither depends on how many timer callbacks the browser decided to actually run in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/page-visibility-api-tab-aware-timers/quiz" rel="noopener noreferrer"&gt;Take the 7-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Focus and visibility are different questions — use the one you mean
&lt;/h2&gt;

&lt;p&gt;The rule of thumb: &lt;code&gt;focus&lt;/code&gt;/&lt;code&gt;blur&lt;/code&gt; for "does this element or window currently have keyboard input" — form fields, keyboard shortcuts, anything about &lt;em&gt;input&lt;/em&gt;. &lt;code&gt;visibilitychange&lt;/code&gt; for "can a human currently see this" — video and animation playback, polling and refresh intervals, analytics dwell time, autosave cadence. It's also the more reliable place to flush a last-gasp analytics beacon than &lt;code&gt;beforeunload&lt;/code&gt;, since &lt;code&gt;hidden&lt;/code&gt; fires on tab close too, and far more consistently across mobile browsers.&lt;/p&gt;

&lt;p&gt;Confusing the two isn't a syntax error. It's a background video that stops for the wrong reason, and a poll that never stops for the right one.&lt;/p&gt;

&lt;p&gt;What's ticking away in one of your background tabs right now that has no idea nobody's watching?&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>browser</category>
      <category>performance</category>
    </item>
    <item>
      <title>NestJS Dependency Injection Explained (with Cheat Sheet)</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Fri, 04 Sep 2026 10:58:05 +0000</pubDate>
      <link>https://dev.to/parsajiravand/nestjs-dependency-injection-explained-with-cheat-sheet-4bem</link>
      <guid>https://dev.to/parsajiravand/nestjs-dependency-injection-explained-with-cheat-sheet-4bem</guid>
      <description>&lt;p&gt;Two feature modules, &lt;code&gt;OrdersModule&lt;/code&gt; and &lt;code&gt;BillingModule&lt;/code&gt;, both list &lt;code&gt;CacheService&lt;/code&gt; in their &lt;code&gt;providers&lt;/code&gt; array. Both inject it in a constructor. Both call &lt;code&gt;cache.increment('hits')&lt;/code&gt;. In staging, the counter never goes above the value each module produced on its own — &lt;code&gt;OrdersModule&lt;/code&gt; reports 40 hits, &lt;code&gt;BillingModule&lt;/code&gt; reports 12, and the total the dashboard shows is wrong by definition, because there's no single counter to be wrong about.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CacheService&lt;/code&gt; is decorated with &lt;code&gt;@Injectable()&lt;/code&gt;. Nobody set a scope. By every definition you've read, it's a singleton. It is — just not the singleton you assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll learn
&lt;/h2&gt;

&lt;p&gt;By the end of this article you'll be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explain what NestJS's dependency injection (DI) container actually does when it sees &lt;code&gt;constructor(private readonly cache: CacheService)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;State precisely what "singleton" means in Nest — and why the same &lt;code&gt;@Injectable()&lt;/code&gt; class can end up as two separate instances&lt;/li&gt;
&lt;li&gt;Choose between &lt;code&gt;useValue&lt;/code&gt;, &lt;code&gt;useClass&lt;/code&gt;, &lt;code&gt;useFactory&lt;/code&gt;, and &lt;code&gt;useExisting&lt;/code&gt; when a provider needs more than a bare class&lt;/li&gt;
&lt;li&gt;Pick the right provider scope (&lt;code&gt;DEFAULT&lt;/code&gt;, &lt;code&gt;REQUEST&lt;/code&gt;, &lt;code&gt;TRANSIENT&lt;/code&gt;) and predict the performance and correctness consequences of each&lt;/li&gt;
&lt;li&gt;Recognize when a provider's scope "bubbles up" and forces something else in your app to become request-scoped too&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;You've written at least one NestJS service with &lt;code&gt;@Injectable()&lt;/code&gt; and injected it into a controller's constructor. You don't need to have written a custom provider, a factory, or a scoped provider yet — we'll build all three from nothing.&lt;/p&gt;

&lt;p&gt;This article is written against &lt;strong&gt;NestJS 12.x&lt;/strong&gt; (verified against the &lt;code&gt;nestjs/nest&lt;/code&gt; GitHub release history — &lt;code&gt;v12.0.0&lt;/code&gt; shipped August 27, 2026). Provider registration and scopes are core-container behavior, unchanged in shape across the 10.x → 12.x line; nothing here depends on the v12 ESM migration specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The problem: a singleton that isn't&lt;/li&gt;
&lt;li&gt;The mental model: registrations, not classes&lt;/li&gt;
&lt;li&gt;Stage 1: the simplest provider&lt;/li&gt;
&lt;li&gt;Stage 2: sharing one instance across modules&lt;/li&gt;
&lt;li&gt;Stage 3: custom providers — when a class isn't enough&lt;/li&gt;
&lt;li&gt;Stage 4: provider scopes — DEFAULT, REQUEST, TRANSIENT&lt;/li&gt;
&lt;li&gt;Stage 5: scope bubbling&lt;/li&gt;
&lt;li&gt;Edge cases and gotchas&lt;/li&gt;
&lt;li&gt;Best practices&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;Cheat sheet&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem: a singleton that isn't
&lt;/h2&gt;

&lt;p&gt;Here's the setup, trimmed to the part that matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cache.service.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CacheService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// orders.module.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;OrdersController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;OrdersService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CacheService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// billing.module.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;BillingController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;BillingService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CacheService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BillingModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;OrdersService&lt;/code&gt; and &lt;code&gt;BillingService&lt;/code&gt; inject &lt;code&gt;CacheService&lt;/code&gt; through their constructors. Both trust that "singleton" means what it usually means in a dependency-injection framework: one instance, shared by whoever asks for it. That trust is reasonable — and wrong here, because of one detail that's easy to skim past: &lt;strong&gt;&lt;code&gt;CacheService&lt;/code&gt; appears in the &lt;code&gt;providers&lt;/code&gt; array of two different modules.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nest doesn't ask "has this class been instantiated anywhere in the app?" It asks "has this token been registered in &lt;em&gt;this module's&lt;/em&gt; injector?" &lt;code&gt;OrdersModule&lt;/code&gt; and &lt;code&gt;BillingModule&lt;/code&gt; never import each other or a shared module that exports &lt;code&gt;CacheService&lt;/code&gt;, so Nest treats the two listings as two independent registrations — and builds two independent instances. Each service gets a real, working, entirely singleton &lt;code&gt;CacheService&lt;/code&gt;. They're just not the &lt;em&gt;same&lt;/em&gt; one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: registrations, not classes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The mental model:&lt;/strong&gt; NestJS's DI container isn't one global map from class to instance. It's a &lt;strong&gt;tree of injectors, one per module&lt;/strong&gt;, and each injector only knows about the providers &lt;em&gt;that module registered&lt;/em&gt; — either directly in its own &lt;code&gt;providers&lt;/code&gt; array, or indirectly, imported from another module that &lt;code&gt;export&lt;/code&gt;s them.&lt;/p&gt;

&lt;p&gt;A provider's real identity is its &lt;strong&gt;token&lt;/strong&gt; (by default, the class itself) &lt;em&gt;plus&lt;/em&gt; &lt;strong&gt;where it was registered&lt;/strong&gt;. "Singleton" is a promise about a registration, not about a class name: &lt;em&gt;within one injector's scope, this token resolves to one instance, created once.&lt;/em&gt; If a class gets registered twice — once per module, with no import/export connecting the two — you get two injectors, two registrations, two honestly-singleton instances that have never met.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;exports&lt;/code&gt; matters so much in Nest, and it's the missing piece in the bug above: to actually share one &lt;code&gt;CacheService&lt;/code&gt;, exactly one module should own it and export it, and every consumer should import that module instead of re-listing the class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cache.module.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CacheService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CacheService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CacheModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// orders.module.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CacheModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;OrdersController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;OrdersService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// CacheService is NOT listed here&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// billing.module.ts&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CacheModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;BillingController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;BillingService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// CacheService is NOT listed here either&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BillingModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now there's exactly one registration of &lt;code&gt;CacheService&lt;/code&gt;, owned by &lt;code&gt;CacheModule&lt;/code&gt;. &lt;code&gt;OrdersModule&lt;/code&gt; and &lt;code&gt;BillingModule&lt;/code&gt; both import it, so Nest resolves the same token to the same instance in both — one counter, correctly shared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; if you want one instance across your app, register the provider in exactly one place and &lt;em&gt;import&lt;/em&gt; it everywhere else. Never re-list the class in a second module's &lt;code&gt;providers&lt;/code&gt; array "to be safe" — that's the line that creates the second instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/nestjs-weekly-dependency-injection-providers-scopes/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: the simplest provider
&lt;/h2&gt;

&lt;p&gt;The smallest possible provider is just a decorated class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GreetingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@Injectable()&lt;/code&gt; marks the class as something Nest's container is allowed to manage. Listing it in a module's &lt;code&gt;providers&lt;/code&gt; array registers it — Nest instantiates it once, resolving its own constructor dependencies first (constructor injection is recursive: if &lt;code&gt;GreetingService&lt;/code&gt; needed a &lt;code&gt;LoggerService&lt;/code&gt;, Nest would build that first). Anything that injects &lt;code&gt;GreetingService&lt;/code&gt; via its constructor type gets the same instance, because the class itself doubles as its injection &lt;strong&gt;token&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: sharing one instance across modules
&lt;/h2&gt;

&lt;p&gt;Covered above — the fix is &lt;code&gt;exports&lt;/code&gt; plus &lt;code&gt;imports&lt;/code&gt;, not a second &lt;code&gt;providers&lt;/code&gt; listing. It's worth restating as a rule, because it's the single most common DI mistake in a growing Nest app: &lt;strong&gt;a provider is shared by being exported and imported, never by being declared twice.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: custom providers — when a class isn't enough
&lt;/h2&gt;

&lt;p&gt;Not every dependency is "a class Nest can &lt;code&gt;new&lt;/code&gt; up." Configuration objects, third-party SDK clients, and values that depend on other providers all need something more flexible than the shorthand &lt;code&gt;providers: [CacheService]&lt;/code&gt;. Nest's provider registration accepts a full object instead, keyed by &lt;code&gt;provide&lt;/code&gt; (the token) and one of four resolution strategies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// useValue — hand Nest an already-built value&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// useClass — pick the implementation at registration time&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentsGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StripeGateway&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// useFactory — build the value at runtime, with its own dependencies&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;useFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ConfigService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DB_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ConfigService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// useExisting — an alias: a second token pointing at the same instance&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LEGACY_CACHE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useExisting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheService&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;'API_BASE_URL'&lt;/code&gt; and &lt;code&gt;'DB_CONNECTION'&lt;/code&gt; are &lt;strong&gt;string tokens&lt;/strong&gt; — the class-as-token trick only works when the dependency is a class, so a plain value or an interface needs an explicit token instead. Injecting one of these requires &lt;code&gt;@Inject()&lt;/code&gt;, since there's no type for Nest to read off the constructor parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentsService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; &lt;code&gt;useValue&lt;/code&gt;, &lt;code&gt;useClass&lt;/code&gt;, &lt;code&gt;useFactory&lt;/code&gt;, and &lt;code&gt;useExisting&lt;/code&gt; are four ways to answer the same question — "what does this token resolve to?" — not four unrelated features. &lt;code&gt;useFactory&lt;/code&gt;'s &lt;code&gt;inject&lt;/code&gt; array is exactly the same resolution the container already does for constructors; it's just spelled out explicitly because a factory function has no constructor for Nest to inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: provider scopes — DEFAULT, REQUEST, TRANSIENT
&lt;/h2&gt;

&lt;p&gt;Everything so far assumes the default: one instance, created once at bootstrap, reused for the life of the process. That's &lt;code&gt;Scope.DEFAULT&lt;/code&gt;, and you never write it — it's what &lt;code&gt;@Injectable()&lt;/code&gt; means with no options. Two other scopes exist, each trading that simplicity for something a shared singleton can't do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REQUEST&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RequestContextService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A new instance is created for every incoming request,&lt;/span&gt;
  &lt;span class="c1"&gt;// and garbage-collected once that request finishes.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TRANSIENT&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoggerService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A new instance is created for every consumer that injects it —&lt;/span&gt;
  &lt;span class="c1"&gt;// not shared, not tied to a request.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;REQUEST&lt;/code&gt; scope is for state that's genuinely per-request — the authenticated user, a request ID for tracing, tenant context in a multi-tenant app. &lt;code&gt;TRANSIENT&lt;/code&gt; is for the rarer case where you don't want sharing at all, even within one request — a logger that should carry the name of whichever class asked for it is the textbook example.&lt;/p&gt;

&lt;p&gt;Both cost something a singleton doesn't. NestJS's own documentation is explicit that request-scoped providers affect performance, because the container can no longer build the dependency graph once at bootstrap — it has to rebuild the request-scoped branch on every request. A properly designed app shouldn't lose more than roughly 5% latency to it, but "properly designed" is doing real work in that sentence: reach for &lt;code&gt;REQUEST&lt;/code&gt; scope only for state that actually varies per request, not as a default habit.&lt;/p&gt;

&lt;p&gt;For multi-tenant apps where many requests share the same tenant, Nest also supports &lt;strong&gt;durable providers&lt;/strong&gt; — &lt;code&gt;@Injectable({ scope: Scope.REQUEST, durable: true })&lt;/code&gt; — which let the container reuse a request-scoped sub-tree across requests that share a common attribute (like a tenant ID) instead of rebuilding it every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: scope bubbling
&lt;/h2&gt;

&lt;p&gt;Scopes aren't isolated to the provider that declares them. If &lt;code&gt;OrdersController&lt;/code&gt; injects &lt;code&gt;RequestContextService&lt;/code&gt; (request-scoped) directly, &lt;code&gt;OrdersController&lt;/code&gt; itself becomes request-scoped — Nest has to create a new controller instance per request too, because it can't build a &lt;code&gt;DEFAULT&lt;/code&gt;-scoped controller once and hand it a dependency that only exists per-request. This is called &lt;strong&gt;scope bubbling&lt;/strong&gt;: request scope propagates up the entire chain of things that (directly or transitively) depend on it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TRANSIENT&lt;/code&gt; doesn't bubble the same way. A &lt;code&gt;DEFAULT&lt;/code&gt;-scoped service that injects a &lt;code&gt;TRANSIENT&lt;/code&gt; logger stays &lt;code&gt;DEFAULT&lt;/code&gt;-scoped — it just gets its own private logger instance, created once, same as any other dependency at bootstrap. Transience only means "not shared between different consumers," not "recreated per request."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; before marking any provider &lt;code&gt;REQUEST&lt;/code&gt;-scoped, check what already depends on it. One request-scoped leaf can turn an entire branch of your app — including controllers — into something rebuilt on every single request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two &lt;code&gt;providers&lt;/code&gt; listings, zero shared state.&lt;/strong&gt; The bug that opened this article. If a "singleton" seems to be losing state, check whether it's registered in more than one module instead of exported from one and imported everywhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circular provider dependencies.&lt;/strong&gt; If &lt;code&gt;ServiceA&lt;/code&gt; needs &lt;code&gt;ServiceB&lt;/code&gt; and &lt;code&gt;ServiceB&lt;/code&gt; needs &lt;code&gt;ServiceA&lt;/code&gt;, Nest can't decide which to build first. &lt;code&gt;forwardRef(() =&amp;gt; ServiceB)&lt;/code&gt; on both sides breaks the deadlock — but a true circular dependency between services is usually a sign one of them should be split.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Injecting a request-scoped provider into a &lt;code&gt;DEFAULT&lt;/code&gt;-scoped one you don't control&lt;/strong&gt; (a library service, for instance) silently makes &lt;em&gt;that&lt;/em&gt; dependency chain request-scoped too, even though nothing about its own code changed. The bubbling happens at the injection site, not the declaration site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useFactory&lt;/code&gt; dependencies must be listed in &lt;code&gt;inject&lt;/code&gt;, in the same order as the factory's parameters.&lt;/strong&gt; Nest resolves them positionally; a factory that takes &lt;code&gt;(config, logger)&lt;/code&gt; but declares &lt;code&gt;inject: [LoggerService, ConfigService]&lt;/code&gt; will hand each argument the wrong provider, with no error — just quietly wrong values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String tokens collide across modules if you're not careful.&lt;/strong&gt; &lt;code&gt;'CACHE'&lt;/code&gt; in one module and &lt;code&gt;'CACHE'&lt;/code&gt; in another are the &lt;em&gt;same&lt;/em&gt; token as far as a shared injector is concerned. Prefer a &lt;code&gt;Symbol()&lt;/code&gt; or an app-wide constants file for non-class tokens once you have more than a couple.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default to &lt;code&gt;Scope.DEFAULT&lt;/code&gt;.&lt;/strong&gt; It's the fastest option and correct for the overwhelming majority of providers — anything that doesn't hold per-request state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own shared providers in one module, export them, and import that module everywhere else.&lt;/strong&gt; Never re-declare the same class in two &lt;code&gt;providers&lt;/code&gt; arrays as a shortcut.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach for &lt;code&gt;REQUEST&lt;/code&gt; scope only for data that's truly per-request&lt;/strong&gt; (the current user, a correlation ID, tenant context) — and remember it will make everything upstream of it request-scoped too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach for &lt;code&gt;TRANSIENT&lt;/code&gt; scope only when sharing would actually cause a bug&lt;/strong&gt; — a logger that should identify its caller is the common case; most services don't need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use string/symbol tokens for anything that isn't a class&lt;/strong&gt; — config values, third-party clients, interfaces — and keep them in one place so two modules never accidentally collide on the same string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In tests, override providers rather than constructing real ones.&lt;/strong&gt; &lt;code&gt;Test.createTestingModule({...}).overrideProvider(CacheService).useValue(fakeCache)&lt;/code&gt; swaps a token's resolution for a test double without touching how the rest of the module is wired — it's the exact same token/registration mechanism this article covers, aimed at a test double instead of the real class.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/nestjs-weekly-dependency-injection-providers-scopes/quiz" rel="noopener noreferrer"&gt;Take the 9-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is a NestJS provider actually a singleton?
&lt;/h3&gt;

&lt;p&gt;Within one registration, yes — &lt;code&gt;DEFAULT&lt;/code&gt; scope guarantees one instance for the life of the app for that specific registration. It is not automatically an app-wide singleton if the same class is registered separately in more than one module; that produces multiple, independently "singleton" instances.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I get two different instances of the same service on purpose?
&lt;/h3&gt;

&lt;p&gt;Either mark it &lt;code&gt;Scope.TRANSIENT&lt;/code&gt; (a fresh instance per consumer), or register it twice under two different tokens using &lt;code&gt;useClass&lt;/code&gt; — e.g. &lt;code&gt;{ provide: 'PRIMARY_DB', useClass: DbConnection }&lt;/code&gt; and &lt;code&gt;{ provide: 'REPLICA_DB', useClass: DbConnection }&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does exporting a provider create a new instance?
&lt;/h3&gt;

&lt;p&gt;No. &lt;code&gt;exports&lt;/code&gt; doesn't instantiate anything — it makes an existing registration visible to modules that &lt;code&gt;import&lt;/code&gt; the module doing the exporting. The instance is still created once, by whichever module has it in &lt;code&gt;providers&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does &lt;code&gt;@Inject()&lt;/code&gt; show up on some constructor parameters and not others?
&lt;/h3&gt;

&lt;p&gt;Nest can use a class as its own injection token automatically, because TypeScript's type metadata gives it something to match. A string, symbol, or interface token has no runtime type to read, so &lt;code&gt;@Inject('TOKEN')&lt;/code&gt; tells Nest explicitly what to resolve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does REQUEST scope work the same way in WebSocket gateways and microservices?
&lt;/h3&gt;

&lt;p&gt;Request-scoped providers are supported outside plain HTTP controllers too, but "request" means whatever triggers a handler in that transport (a socket event, a message) — always check that the perf tradeoff still makes sense for a transport that may see much higher throughput than typical HTTP traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic provider&lt;/td&gt;
&lt;td&gt;&lt;code&gt;providers: [MyService]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shorthand for &lt;code&gt;{ provide: MyService, useClass: MyService }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share one instance across modules&lt;/td&gt;
&lt;td&gt;Export from an owner module, &lt;code&gt;imports&lt;/code&gt; it elsewhere&lt;/td&gt;
&lt;td&gt;Never re-list the class in a second &lt;code&gt;providers&lt;/code&gt; array&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provide a plain value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ provide: 'TOKEN', useValue: x }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Needs &lt;code&gt;@Inject('TOKEN')&lt;/code&gt; at the injection site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap implementations&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ provide: Base, useClass: Impl }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consumers still inject &lt;code&gt;Base&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build at runtime with deps&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ provide: 'X', useFactory: fn, inject: [...] }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;inject&lt;/code&gt; order must match &lt;code&gt;fn&lt;/code&gt;'s parameter order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alias an existing token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ provide: 'ALIAS', useExisting: Real }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same instance, second name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One instance for the app&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Injectable()&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Scope.DEFAULT&lt;/code&gt;, built once at bootstrap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One instance per request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Injectable({ scope: Scope.REQUEST })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bubbles up to every consumer; ~5% latency cost when used narrowly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One instance per consumer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Injectable({ scope: Scope.TRANSIENT })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Doesn't bubble; each injector gets its own copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reuse a REQUEST sub-tree by tenant&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Injectable({ scope: Scope.REQUEST, durable: true })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multi-tenant optimization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Override in tests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Test.createTestingModule().overrideProvider(X).useValue(fake)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same token mechanism, aimed at a test double&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A provider's identity in Nest is &lt;strong&gt;its token plus its registration&lt;/strong&gt; — not just its class name. The same class registered in two modules is two instances.&lt;/li&gt;
&lt;li&gt;Share one instance by &lt;strong&gt;exporting it from a single owning module&lt;/strong&gt; and importing that module everywhere it's needed — never by listing the class twice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useValue&lt;/code&gt;, &lt;code&gt;useClass&lt;/code&gt;, &lt;code&gt;useFactory&lt;/code&gt;, and &lt;code&gt;useExisting&lt;/code&gt; are four answers to "what does this token resolve to," and non-class tokens need &lt;code&gt;@Inject()&lt;/code&gt; because there's no type for Nest to read.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;REQUEST&lt;/code&gt; and &lt;code&gt;TRANSIENT&lt;/code&gt; scope solve real problems, but &lt;code&gt;REQUEST&lt;/code&gt; scope &lt;strong&gt;bubbles up&lt;/strong&gt; the entire dependency chain and comes with a real, if usually small, performance cost — reach for &lt;code&gt;DEFAULT&lt;/code&gt; unless you specifically need per-request state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;CacheService&lt;/code&gt; bug from the top of this article has a one-line fix — move it into an exported, imported &lt;code&gt;CacheModule&lt;/code&gt; — but the DI container doesn't tell you that's the problem. It just quietly builds what you asked for: two registrations, two instances, two counters, both correct on their own and wrong together. Once you're reading "singleton" as "one instance per registration" instead of "one instance in the app," that class of bug stops being a mystery and starts being something you check for on sight.&lt;/p&gt;

&lt;p&gt;What's the DI bug that cost you the most time to track down — a duplicate registration, a scope that bubbled somewhere you didn't expect, or something else? Drop it in the comments.&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nestjs</category>
      <category>node</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Regex-Parsing document.cookie. Use CookieStore</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Fri, 04 Sep 2026 10:57:34 +0000</pubDate>
      <link>https://dev.to/parsajiravand/stop-regex-parsing-documentcookie-use-cookiestore-5c41</link>
      <guid>https://dev.to/parsajiravand/stop-regex-parsing-documentcookie-use-cookiestore-5c41</guid>
      <description>&lt;p&gt;Open any codebase old enough to have cookies in it and grep for &lt;code&gt;document.cookie&lt;/code&gt;. You will find a function that looks like this, written by someone who is no longer at the company:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(^| )&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=([^;]+)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It has worked since 2011. It also has a small, well-known list of ways to get it wrong — cookie names that are prefixes of each other, values with unescaped &lt;code&gt;=&lt;/code&gt; or &lt;code&gt;;&lt;/code&gt;, whitespace after the semicolon depending on which browser wrote the header. Everyone's seen at least one of these bugs. Nobody rewrites the function, because it's not broken &lt;em&gt;today&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's the harder problem that regex can't fix at all: &lt;strong&gt;you have no way to know when a cookie changes.&lt;/strong&gt; Not from another tab. Not from a &lt;code&gt;Set-Cookie&lt;/code&gt; header on a &lt;code&gt;fetch()&lt;/code&gt; response. Not even from a second script on your own page calling &lt;code&gt;document.cookie = ...&lt;/code&gt; a moment after yours did. &lt;code&gt;document.cookie&lt;/code&gt; is a plain string property. Reading it tells you the current state. It has never told you &lt;em&gt;when&lt;/em&gt; the state moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What everyone reaches for instead
&lt;/h2&gt;

&lt;p&gt;Once the "I need to react to cookie changes" requirement shows up — a login cookie set by an API call, a consent banner another tab just dismissed — the usual fixes are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Poll &lt;code&gt;document.cookie&lt;/code&gt; on an interval.&lt;/strong&gt; It works, in the sense that a &lt;code&gt;setInterval&lt;/code&gt; checking a string every 500ms will eventually notice a change. It also means every tab of every user is now diffing a string forever for an event that might happen once a session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have the server tell the client via a WebSocket or SSE.&lt;/strong&gt; Real infrastructure for a problem that's purely local — the cookie already changed on this machine, in this browser, you just don't have a hook for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap every single place that writes a cookie in your own pub/sub.&lt;/strong&gt; This can work, right up until a third-party script, a &lt;code&gt;Set-Cookie&lt;/code&gt; response header, or literally the browser's own cookie-jar expiry logic changes a cookie your pub/sub doesn't know about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three treat "the browser won't tell me" as something to engineer around. It's worth asking why the browser won't tell you in the first place — and it turns out, more recently, it will.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API that actually does this: &lt;code&gt;cookieStore&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Chrome and Edge ship &lt;code&gt;window.cookieStore&lt;/code&gt; (and &lt;code&gt;self.cookieStore&lt;/code&gt; inside a service worker) — a promise-based Cookie Store API that treats cookies as structured objects instead of one string you serialize by hand.&lt;/p&gt;

&lt;p&gt;Reading is no longer a regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { name: "session_id", value: "abc123", domain: null, path: "/", ... } or null&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// array of every cookie visible to this document, already parsed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing takes an object instead of a hand-built &lt;code&gt;key=value; path=...; expires=...&lt;/code&gt; string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30 days, in ms&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the part &lt;code&gt;document.cookie&lt;/code&gt; could never do — a real event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;set:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deleted:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That listener fires for cookies your page sets, cookies a &lt;code&gt;fetch()&lt;/code&gt; response set via &lt;code&gt;Set-Cookie&lt;/code&gt;, and cookies removed by expiry — no polling, no pub/sub you wrote yourself. A consent banner that gets dismissed in one tab can now update every other open tab of the same origin the moment it happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/cookie-store-api-async-cookies/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The default that's stricter than you're used to
&lt;/h2&gt;

&lt;p&gt;Here's the thing that actually catches people moving existing code over, and it's not a bug — it's a deliberate spec choice that reads like a footnote until it breaks something.&lt;/p&gt;

&lt;p&gt;When you write a cookie the old way, through a &lt;code&gt;Set-Cookie&lt;/code&gt; header or &lt;code&gt;document.cookie&lt;/code&gt;, and you don't specify &lt;code&gt;SameSite&lt;/code&gt;, browsers default it to &lt;code&gt;Lax&lt;/code&gt;. That's been true for years — it's why a cookie set on your site still rides along when a user clicks a plain link &lt;em&gt;to&lt;/em&gt; your site from somewhere else, but doesn't get sent on a cross-site &lt;code&gt;POST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cookieStore.set()&lt;/code&gt; doesn't inherit that default. Per the spec, if you don't pass &lt;code&gt;sameSite&lt;/code&gt; explicitly, it defaults to &lt;strong&gt;&lt;code&gt;"strict"&lt;/code&gt;&lt;/strong&gt; — stricter than what &lt;code&gt;document.cookie&lt;/code&gt; gives you for free. A &lt;code&gt;Strict&lt;/code&gt; cookie is withheld on &lt;em&gt;any&lt;/em&gt; cross-site navigation, top-level link clicks included.&lt;/p&gt;

&lt;p&gt;So the failure mode looks like this: you migrate a cookie-setting line from &lt;code&gt;document.cookie = "..."&lt;/code&gt; to &lt;code&gt;cookieStore.set({...})&lt;/code&gt;, run your test suite, ship it. Everything that happens &lt;em&gt;inside&lt;/em&gt; your own site keeps working, because same-site requests don't care about &lt;code&gt;SameSite&lt;/code&gt; at all. Weeks later, someone clicks a link to your site from an email or a partner site, lands on a page that expects that cookie to already be there, and it isn't. No error. No console warning. The cookie you set is simply not attached to that request, because &lt;code&gt;Strict&lt;/code&gt; said not to.&lt;/p&gt;

&lt;p&gt;The fix is one keyword, once you know to look for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cookieStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// match what document.cookie would have given you&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two things worth knowing before you reach for it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's Chromium-only right now.&lt;/strong&gt; Chrome and Edge support &lt;code&gt;cookieStore&lt;/code&gt;; Firefox and Safari don't ship it as of this writing. Check the current numbers on caniuse before you rely on it for anything that isn't wrapped in a feature check — &lt;code&gt;if ("cookieStore" in window)&lt;/code&gt; — with a &lt;code&gt;document.cookie&lt;/code&gt; fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It requires a secure context.&lt;/strong&gt; Like most newer, more capable browser APIs, &lt;code&gt;cookieStore&lt;/code&gt; simply isn't there on plain &lt;code&gt;http://&lt;/code&gt; origins outside &lt;code&gt;localhost&lt;/code&gt;. If it's &lt;code&gt;undefined&lt;/code&gt; in production but present when you test locally, that's almost certainly why.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;document.cookie&lt;/code&gt; was never designed to be parsed — it's a string interface bolted onto a feature that predates &lt;code&gt;JSON.parse&lt;/code&gt; existing. &lt;code&gt;cookieStore&lt;/code&gt; treats cookies as the structured, awaitable, observable data they actually are, and the &lt;code&gt;change&lt;/code&gt; event alone is worth the migration for anything that needs to react to a cookie set outside your own code. Just don't let &lt;code&gt;sameSite&lt;/code&gt; default silently to something stricter than the behavior you were relying on.&lt;/p&gt;

&lt;p&gt;Does your codebase still have a hand-rolled cookie parser in it? How old is it, and does anyone remember writing it?&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/cookie-store-api-async-cookies/quiz" rel="noopener noreferrer"&gt;Take the 7-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>browser</category>
      <category>frontend</category>
    </item>
    <item>
      <title>The await That Silently Breaks navigator.clipboard.writeText()</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:23:31 +0000</pubDate>
      <link>https://dev.to/parsajiravand/the-await-that-silently-breaks-navigatorclipboardwritetext-10oe</link>
      <guid>https://dev.to/parsajiravand/the-await-that-silently-breaks-navigatorclipboardwritetext-10oe</guid>
      <description>&lt;p&gt;Someone on your team ships a "Copy invite link" button. It fetches a fresh, single-use link from the API, then copies it to the clipboard so the user can paste it into Slack. Code review is clean. QA clicks it a dozen times. Works every time.&lt;/p&gt;

&lt;p&gt;Two weeks later, a support ticket: "I click Copy, I paste into Slack, and I get yesterday's clipboard contents. Not the link." Another ticket, same shape, different user. You can't reproduce it. You click the button forty times in a row and it copies the link forty times.&lt;/p&gt;

&lt;p&gt;Here's the detail that breaks the case open, if you know to look for it: every user who hit this had switched to another tab or clicked into another window in the second or two between clicking Copy and the link actually landing on their clipboard. Nothing crashed. No error reached the UI. &lt;code&gt;navigator.clipboard.writeText()&lt;/code&gt; just quietly declined to run, and the code never checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes that don't touch it
&lt;/h2&gt;

&lt;p&gt;The instinct is to treat it as a normal race condition or a network hiccup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a loading spinner so the user waits for the fetch to finish before clicking.&lt;/strong&gt; Doesn't help — the bug isn't about clicking too early, it's about what happens &lt;em&gt;after&lt;/em&gt; the click, while your code is still awaiting something.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap the write in &lt;code&gt;try/catch&lt;/code&gt; and just eat the error.&lt;/strong&gt; Now it fails the same amount, but silently on purpose instead of silently by accident. Worse, arguably — you've deleted your own evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry the write a moment later.&lt;/strong&gt; If the reason it failed is still true (the document still isn't focused), the retry fails too. If you retry indefinitely, you've built a poller for a permission that has nothing to do with time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these ask the one useful question: &lt;em&gt;why does a browser API that "just copies a string" refuse to run at all?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The real rule: the API only trusts the instant of the click
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;navigator.clipboard.writeText()&lt;/code&gt; is part of the Async Clipboard API, and it enforces something stricter than "the user clicked a button once, somewhere." At the exact moment you call it, the browser wants two things to still be true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The document has focus.&lt;/strong&gt; Not "had focus when the click happened" — has it &lt;em&gt;right now&lt;/em&gt;, this call, this tick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user gesture is still active.&lt;/strong&gt; Clicks grant a short-lived window of "the user just did something," and that window doesn't wait around forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Await literally anything — a &lt;code&gt;fetch()&lt;/code&gt; for the link, a promise chain, even a couple of re-renders — and you've inserted a gap between the click and the actual &lt;code&gt;writeText()&lt;/code&gt; call. If the user alt-tabs, clicks a browser chrome element, or a dev-tools panel steals focus during that gap, the call arrives with the document unfocused. The browser doesn't queue it, doesn't warn the user, doesn't retry. It rejects the promise with a &lt;code&gt;NotAllowedError&lt;/code&gt; — in Chrome, literally &lt;code&gt;"Failed to execute 'writeText' on 'Clipboard': Document is not focused."&lt;/code&gt; — and if nothing in your code reads that rejection, it vanishes into an unhandled-promise-rejection log line nobody watches production for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// looks completely reasonable, fails silently under real-world timing&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;copyInviteLink&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/invite-link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;- the gap opens here&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// &amp;lt;- and this is what falls in it&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here is wrong syntax. It's wrong &lt;em&gt;sequencing&lt;/em&gt; — the write happens whenever the network happens to resolve, not while the click is still fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/clipboard-writetext-focus-bug/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: give the API the gesture immediately, the data later
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;writeText()&lt;/code&gt; is a convenience method — it only accepts a string you already have in hand, right now. But its sibling, &lt;code&gt;navigator.clipboard.write()&lt;/code&gt;, takes a &lt;code&gt;ClipboardItem&lt;/code&gt;, and a &lt;code&gt;ClipboardItem&lt;/code&gt;'s data doesn't have to be a plain string or &lt;code&gt;Blob&lt;/code&gt;. It can be a &lt;strong&gt;Promise&lt;/strong&gt; that resolves to one later. That's the actual tool for "I have the user's permission right now, but not the text yet":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// call write() synchronously, in the same tick as the click —&lt;/span&gt;
&lt;span class="c1"&gt;// the *data* is allowed to arrive whenever it's ready&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;copyInviteLink&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linkPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/invite-link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ClipboardItem&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;linkPromise&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;write()&lt;/code&gt; call itself still happens inside the click handler, before any &lt;code&gt;await&lt;/code&gt; has had a chance to let focus slip — so the permission check passes at the one moment it's guaranteed to be true. The browser holds the clipboard slot open and fills it in once your promise settles. Nothing about your fetch logic has to change; only &lt;em&gt;which method&lt;/em&gt; you hand the eventual string to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two details worth knowing before you ship this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The whole API requires a secure context.&lt;/strong&gt; &lt;code&gt;navigator.clipboard&lt;/code&gt; simply isn't there on plain &lt;code&gt;http://&lt;/code&gt; origins (outside &lt;code&gt;localhost&lt;/code&gt;) — if it's &lt;code&gt;undefined&lt;/code&gt; in production but not on your machine, that's almost always why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;document.execCommand("copy")&lt;/code&gt; is deprecated&lt;/strong&gt;, per MDN, and not guaranteed to work or even exist in every browser going forward. It's still floating around in older code because it predates the Async Clipboard API and doesn't have this exact focus problem — it copies synchronously from a selection, no promise involved — but it's not the thing to reach for in new code, deprecated fallback or not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one thing worth remembering
&lt;/h2&gt;

&lt;p&gt;A copy button that works in every manual test and fails for real users isn't flaky — it's timing-dependent in a way your test never reproduces, because you never alt-tab mid-click when you're the one testing it. The rule is simple once you name it: keep the call to the Clipboard API synchronous with the gesture that authorizes it, and if the data isn't ready yet, hand the API a promise instead of making it wait for one.&lt;/p&gt;

&lt;p&gt;Have you shipped a copy-to-clipboard button that "just doesn't work sometimes" and quietly caught the error instead of asking why? What did the try/catch look like?&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/clipboard-writetext-focus-bug/quiz" rel="noopener noreferrer"&gt;Take the 8-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>browser</category>
      <category>frontend</category>
    </item>
    <item>
      <title>You're not quiet in meetings, you're editing</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:58:22 +0000</pubDate>
      <link>https://dev.to/parsajiravand/youre-not-quiet-in-meetings-youre-editing-231m</link>
      <guid>https://dev.to/parsajiravand/youre-not-quiet-in-meetings-youre-editing-231m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;👋 &lt;strong&gt;I'm Parsa Jiravand — I work in IT, and this is Best Practice.&lt;/strong&gt; One article every day, one soft-skills episode every week. It all lives at &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — come join us.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's minute twelve of a fifty-minute meeting and you've got the thing. It's good. It's actually the thing nobody in the room has said yet. You just want to tidy it up a bit so it comes out right.&lt;/p&gt;

&lt;p&gt;Minute nineteen: someone else says about two-thirds of it, worse, and everyone nods.&lt;/p&gt;

&lt;p&gt;Minute fifty: you're making tea, delivering the full polished version to yourself, and it is genuinely better than what they said, and it does not matter even slightly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually going on
&lt;/h2&gt;

&lt;p&gt;You've probably decided this is a confidence problem. I don't think it is, mostly.&lt;/p&gt;

&lt;p&gt;Watch what the people who talk a lot are actually doing. They're not delivering finished thoughts. They're thinking out loud — starting a sentence before they know how it ends and steering it while it's already moving. A decent chunk of what they say is wrong, and they correct it themselves three sentences later, and nobody holds it against them, because that's what the room is for.&lt;/p&gt;

&lt;p&gt;You're doing a different thing. You're drafting. Writing the sentence in your head, checking it, cutting the weak clause, checking again that it doesn't sound stupid — and then submitting it.&lt;/p&gt;

&lt;p&gt;Those are two different activities, and only one of them fits inside a meeting.&lt;/p&gt;

&lt;p&gt;That's the whole gap. It isn't that they're sharper. It's latency. They're running at conversation speed because they've offloaded the thinking into the talking; you're running at writing speed because you're doing the thinking first, in private, to a higher standard than the format requires.&lt;/p&gt;

&lt;p&gt;And here's the mean part. A good thought delivered late arrives worse than a rough one delivered on time. At minute twelve, your half-formed version steers the discussion — it's cheap, it's part of the flow, someone builds on it. At minute thirty, the exact same words are a rewind. You're asking eight people to go back. So now it does have to be brilliant, because you're spending everyone's time to say it. Which makes you edit harder. Which makes it later.&lt;/p&gt;

&lt;p&gt;The other thing, and I think this is the one people genuinely don't notice: &lt;strong&gt;you're waiting for a gap, and there is no gap.&lt;/strong&gt; Silence in a meeting isn't an invitation. It's someone breathing in. The people who get to speak aren't luckier with timing — they start on the in-breath, before it's fully quiet. If you're waiting for a clean, unambiguous, obviously-yours window, you can sit through an entire hour without getting one and conclude afterwards that nobody let you in.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what do you actually say?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claim the floor before the thought is finished.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The move is to spend your thinking time out loud instead of before. Which sounds horrifying, so you buy the first few seconds with a phrase that requires no content at all:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Can I jump in on that one?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's four seconds of runway, and in those four seconds you form the sentence you were going to form anyway. You never needed to be finished. You needed to not be silent while you finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Label it half-formed and it stops having to be right.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Half-formed thought — is this the same problem as the thing with the billing sync?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two things happen there. The label sets the terms: you've told the room this is a draft, so nobody's grading it as a conclusion. And it's a question, which is a complete contribution even when your opinion isn't ready — it moves the discussion without requiring you to be correct about anything.&lt;/p&gt;

&lt;p&gt;That second part matters more than it sounds, because most of the reason people stay quiet is that they're waiting to have an answer. You almost never need an answer. Naming the thing everyone is circling is a contribution: &lt;em&gt;"Are we deciding this now, or are we working out who decides it?"&lt;/em&gt; has saved more meeting-hours than any opinion I've seen delivered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Say something in the first ten minutes. Anything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not a good thing. Any thing. "Can we go back one slide?" counts.&lt;/p&gt;

&lt;p&gt;The mechanic is that the room builds a model of you early — participant or audience — and every minute of silence after that raises the price of your first sentence. At minute three, speaking is nothing. At minute forty, speaking means visibly changing category in front of everyone, which is a far bigger move than the one you were avoiding. People read your first contribution as your register for the meeting. Nobody notices the second one at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the one that does the most work: get in before the meeting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One line to whoever's running it, the day before: &lt;em&gt;"I've got a thing about the migration — want me to raise it, or is that not this meeting?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now you're expected. Being expected is most of being able to speak, because the hard part was never the sentence — it was granting yourself permission to take the room's time, and someone else has now granted it for you. And maybe a third of the time you'll get back "oh, we actually settled that on Friday," which saves you the entire ordeal.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing that's new this year
&lt;/h2&gt;

&lt;p&gt;If your company runs an AI notetaker — and most do now — something is true that wasn't a few years ago. The summary lists decisions, owners, and who raised what. It gets skimmed by people who weren't there. It is the version that survives.&lt;/p&gt;

&lt;p&gt;Which means the mediocre thing you said out loud at minute twelve is in the record, and the excellent version you sent in a DM afterwards isn't. I'm not thrilled about that either. But it's an argument for the cheap early sentence, not against it.&lt;/p&gt;

&lt;p&gt;Worth knowing the other half: those summaries get attribution wrong reasonably often, and there's no appeals process. So if something genuinely needs to be yours, a comment on the doc beforehand is more durable than a spoken line — it's timestamped, it's written down, and it's still there when the transcript decides someone else said it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this backfires
&lt;/h2&gt;

&lt;p&gt;If you're the most senior person in the room, invert all of it. Speaking early doesn't make you a participant, it closes the question — you'll get agreement instead of opinions and you won't be able to tell which one you got. Your version of this is to go last, and to leave a silence long enough to be uncomfortable, and then to hold it, because the useful thing in that room isn't coming from you.&lt;/p&gt;

&lt;p&gt;Second: don't do it to be seen doing it. Talking to appear engaged is one of the most legible things a person can do — nobody can prove it, everybody clocks it. If you speak early and it's filler, you've just spent the credibility you were trying to build. "Any thing" means any &lt;em&gt;real&lt;/em&gt; thing. "Can we go back a slide" is real. Confidently restating what the last person just said is not.&lt;/p&gt;

&lt;p&gt;Third, and this one depends entirely on the room: in some teams, and in some cultures, jumping in is straightforwardly rude, and the actual protocol is the raised hand, or the chat, or waiting to be asked by name. Advice about interrupting is mostly written by people who work in rooms where interrupting is free. Spend one meeting watching how contributions actually get in — that's a real thing to do with a meeting — then use that door instead of this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that isn't fair
&lt;/h2&gt;

&lt;p&gt;Interrupting is not equally priced.&lt;/p&gt;

&lt;p&gt;The same jump-in reads as decisive from one person and rude from another, and it has very little to do with the sentence. If you're the only junior in there, if you're dialled in while five people share a room, if you're the only woman at the table, if you're working in your second language — you are paying more per interruption than the person next to you. Every article that says "just speak up more" is quietly assuming your price is the same as the author's.&lt;/p&gt;

&lt;p&gt;The remote one is partly literal, which I find weirdly comforting. A few hundred milliseconds each way means you physically cannot land in a pause — by the time the silence reaches you and your voice gets back, the room has moved. You're not being hesitant. You're behind by a measurable amount of time.&lt;/p&gt;

&lt;p&gt;Two honest responses to that. The first is that whoever runs the meeting can fix it in one sentence — "let's go round" or "hang on, Sam had something" — and most of them will, if you ask once, privately, in plain words: &lt;em&gt;"I keep getting talked over on the call. Could you come to me by name?"&lt;/em&gt; That's not a complaint, it's a request with a mechanism, and it's much easier to say yes to.&lt;/p&gt;

&lt;p&gt;The second is that writing is a legitimate route and not the consolation prize. A comment on the doc lands in the room whether you speak or not. Some people are simply better in writing, and building your influence around that is a strategy rather than a surrender. The trap is that it only works &lt;em&gt;before&lt;/em&gt;. The same paragraph posted after the meeting is a note. Posted the night before, it's the agenda.&lt;/p&gt;

&lt;p&gt;And the last uncomfortable thing, which took me a long time to see: a lot of decisions weren't made in the meeting at all. Two people talked on Tuesday, and the meeting is where it gets said out loud with everyone present. If you keep bringing good arguments that don't move anything, you may not be bad at meetings — you may be arriving at the ratification. The room to change it was the conversation before it, and getting into &lt;em&gt;that&lt;/em&gt; conversation is a different skill entirely. One for another week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday
&lt;/h2&gt;

&lt;p&gt;Next meeting you're in, say something in the first five minutes.&lt;/p&gt;

&lt;p&gt;It does not have to be good. A question counts. "Can we go back one slide" counts. You're not trying to contribute yet — you're changing category from audience to participant while it's still cheap.&lt;/p&gt;

&lt;p&gt;Then for the meeting after that one, send the line the day before: &lt;em&gt;"I've got a thing about X — want me to raise it, or is that not this meeting?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's the whole practice. You don't need to become more confident. You need to stop editing and start talking at the same speed as everyone else in the room.&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🤝 &lt;strong&gt;Want to make something with us?&lt;/strong&gt; Write a piece, come on the podcast, or bring an idea that should exist — &lt;strong&gt;&lt;a href="https://bestpractic.org/collaborate" rel="noopener noreferrer"&gt;bestpractic.org/collaborate&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And if this one helped, send it to the person you know who needs it this week. That's what keeps these coming.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>discuss</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Shadow DOM Needed JavaScript to Exist. Now It Doesn't.</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:57:52 +0000</pubDate>
      <link>https://dev.to/parsajiravand/shadow-dom-needed-javascript-to-exist-now-it-doesnt-2lla</link>
      <guid>https://dev.to/parsajiravand/shadow-dom-needed-javascript-to-exist-now-it-doesnt-2lla</guid>
      <description>&lt;p&gt;You curl your own page. The one with the fancy web component — the accordion, the tabs, whatever you built to be reusable everywhere. You SSR'd it. You're proud of it. You grep the response for the text inside it.&lt;/p&gt;

&lt;p&gt;It's not there.&lt;/p&gt;

&lt;p&gt;What's there instead is a custom element with nothing inside it, and if you squint at the raw HTML you might spot a &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag nearby, doing nothing. The content — the styled, encapsulated, actually-rendered thing a user sees — doesn't exist yet. It shows up a beat later, after JavaScript runs, which on a slow connection or a busy main thread is long enough to see the host element sit empty, then pop.&lt;/p&gt;

&lt;p&gt;Here's the part that takes people longer to accept than it should: this was never a bug in your SSR setup. Shadow DOM itself had no way to be server-rendered. Not "hard" — structurally absent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious fixes that don't touch the real problem
&lt;/h2&gt;

&lt;p&gt;The instinct, once you notice the flash, is to attack it like a loading-performance problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inline the critical CSS&lt;/strong&gt; so at least the &lt;em&gt;page&lt;/em&gt; doesn't flash unstyled. Doesn't help — the shadow tree's content isn't unstyled, it's &lt;em&gt;absent&lt;/em&gt;. There's nothing to style yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defer less, preload the script earlier.&lt;/strong&gt; Shaves milliseconds. The gap doesn't close, it shrinks — and on a throttled connection or a busy tab it's still visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skeleton-load it&lt;/strong&gt;, show a placeholder shape until hydration. This works, and it's honestly the standard answer today — but notice what you just admitted: you're not server-rendering the component, you're server-rendering an apology for the component and rendering the real thing later, client-side, same as if you'd shipped no HTML at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three treat the symptom. None of them explain &lt;em&gt;why&lt;/em&gt; a server-rendered custom element can serialize its attributes and its light-DOM children into HTML, but not its shadow tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real diagnosis: there was no way to write it down
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;element.attachShadow({ mode: "open" })&lt;/code&gt; is a method call. It runs in JavaScript, against a live DOM node, in a live document. There has never been an HTML syntax for "this element has a shadow root containing this markup" — the shadow tree simply doesn't exist as text you can send over the wire. A server can stringify your component's attributes and its regular children all day. The encapsulated part — the actual point of using Shadow DOM — has nowhere to go in that string.&lt;/p&gt;

&lt;p&gt;So "server-rendered web component" quietly meant: server-rendered &lt;em&gt;light DOM&lt;/em&gt;, client-attached shadow DOM, and a component that isn't really done rendering until JavaScript shows up and calls a method. For a design system trying to sell itself on being framework-agnostic and usable anywhere — including a no-JS environment, a crawler, or a slow device — that's a real gap, not a nitpick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: give the parser something to attach
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://web.dev/articles/declarative-shadow-dom" rel="noopener noreferrer"&gt;Declarative Shadow DOM&lt;/a&gt; closes the gap by teaching the HTML parser a new trick instead of teaching JavaScript a new one. Put a &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; with a &lt;code&gt;shadowrootmode&lt;/code&gt; attribute as the &lt;em&gt;first child&lt;/em&gt; of an element, and the parser attaches its contents as a real, live shadow root the instant it finishes reading the closing tag — before any &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; further down the page has run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- the old way: an empty host, waiting for JS --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;my-badge&amp;gt;&amp;lt;/my-badge&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-badge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-badge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachShadow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="s2"&gt;`&amp;lt;style&amp;gt;span{color:hotpink}&amp;lt;/style&amp;gt;&amp;lt;span&amp;gt;New&amp;lt;/span&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- the new way: the shadow root exists the moment HTML parses --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;my-badge&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;shadowrootmode=&lt;/span&gt;&lt;span class="s"&gt;"open"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;span&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;hotpink&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;New&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/my-badge&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No script needed to make the second version render — &lt;code&gt;document.querySelector("my-badge").shadowRoot&lt;/code&gt; is already non-null before your JavaScript has had a chance to run at all. If you &lt;em&gt;do&lt;/em&gt; still want to progressively enhance it with a custom element class, that's fine: attach-shadow-if-not-already-attached is the standard guard, since a browser that already declaratively created the shadow root won't create a second one for you.&lt;/p&gt;

&lt;p&gt;A couple of details worth knowing before you reach for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shadowrootmode&lt;/code&gt; is &lt;code&gt;"open"&lt;/code&gt; or &lt;code&gt;"closed"&lt;/code&gt;&lt;/strong&gt;, mirroring &lt;code&gt;attachShadow()&lt;/code&gt;'s mode option — &lt;code&gt;"closed"&lt;/code&gt; still renders the content, it just means &lt;code&gt;element.shadowRoot&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; to outside script, same as the imperative API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only the first matching &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; counts.&lt;/strong&gt; If a parent already got a declarative shadow root, a second &lt;code&gt;shadowrootmode&lt;/code&gt; template in the same position is left alone as a plain, inert &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; element rather than throwing — a browsers-are-forgiving default, but not one to rely on if you're generating this HTML yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round-tripping it needs an extra flag.&lt;/strong&gt; If you want to read a component's &lt;em&gt;current&lt;/em&gt; shadow content back out as a string (say, to re-serialize it after a client-side update), add &lt;code&gt;shadowrootserializable&lt;/code&gt; to the template and call &lt;code&gt;getHTML({ serializableShadowRoots: true })&lt;/code&gt; — without it, &lt;code&gt;outerHTML&lt;/code&gt; and friends skip shadow content entirely, same as before.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/declarative-shadow-dom-ssr-web-components/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands today
&lt;/h2&gt;

&lt;p&gt;This isn't a "wait five years" feature. Declarative Shadow DOM is Baseline: Chrome and Edge since version 111 (with the full attribute set standardized by 124), Firefox since 123, Safari since 16.4. If your SSR framework or web component library doesn't emit &lt;code&gt;shadowrootmode&lt;/code&gt; templates yet, that's a library gap, not a browser one — and it's worth filing an issue instead of writing another skeleton loader.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing worth remembering
&lt;/h2&gt;

&lt;p&gt;An empty custom element with a &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; next to it isn't broken HTML — it's HTML written for a browser that hasn't decided to help yet. Declarative Shadow DOM is the browser deciding to help: the parser does at read-time what a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag used to have to do at run-time, and the gap between "the server sent HTML" and "the component actually rendered" gets to close for good.&lt;/p&gt;

&lt;p&gt;Do you SSR web components today — and if so, are you shipping the skeleton-loader workaround or has your framework already picked this up? I'd like to know which libraries got there first.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/declarative-shadow-dom-ssr-web-components/quiz" rel="noopener noreferrer"&gt;Take the 8-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>html</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Next.js Cache Components Explained (with Cheat Sheet)</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:24:14 +0000</pubDate>
      <link>https://dev.to/parsajiravand/nextjs-cache-components-explained-with-cheat-sheet-55ob</link>
      <guid>https://dev.to/parsajiravand/nextjs-cache-components-explained-with-cheat-sheet-55ob</guid>
      <description>&lt;p&gt;You add one line to a layout — &lt;code&gt;const theme = (await cookies()).get('theme')?.value&lt;/code&gt; — to greet returning visitors by their saved preference. Nothing else changes. Deploy, and your blog's server load quadruples: every page that used to serve instantly from a CDN edge now renders fresh, on your origin server, for every single visitor, including the 95% of the page that is identical for everyone.&lt;/p&gt;

&lt;p&gt;Nothing you wrote was wrong, exactly. It's how the App Router's &lt;em&gt;previous&lt;/em&gt; rendering model worked: one dynamic API call anywhere in a route's tree marked the &lt;em&gt;entire&lt;/em&gt; route dynamic. Next.js's new Cache Components model exists specifically to fix this, and understanding how it decides what's static, what's cached, and what streams is the single most valuable thing you can know about the framework right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll learn
&lt;/h2&gt;

&lt;p&gt;By the end of this article you'll be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explain why a single &lt;code&gt;cookies()&lt;/code&gt; or &lt;code&gt;headers()&lt;/code&gt; call used to make a whole Next.js route dynamic, and how Cache Components changes that&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;use cache&lt;/code&gt; directive at the function, component, and file level, and know which one to reach for&lt;/li&gt;
&lt;li&gt;Set explicit cache lifetimes with &lt;code&gt;cacheLife&lt;/code&gt; and invalidate on demand with &lt;code&gt;cacheTag&lt;/code&gt; + &lt;code&gt;updateTag&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Read a route and predict which parts become the static shell, which get cached, and which stream in behind a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary&lt;/li&gt;
&lt;li&gt;Avoid the constraints that trip people up first: reading runtime APIs inside a cached scope, and passing uncached promises into one&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;You've built at least a small App Router project — a &lt;code&gt;page.tsx&lt;/code&gt;, a &lt;code&gt;layout.tsx&lt;/code&gt;, maybe a &lt;code&gt;fetch&lt;/code&gt; call inside a Server Component. You don't need any prior experience with caching APIs; we build the model from nothing.&lt;/p&gt;

&lt;p&gt;This article is written against &lt;strong&gt;Next.js 16.3&lt;/strong&gt; (verified against the framework's own documentation and npm's &lt;code&gt;latest&lt;/code&gt; dist-tag in August 2026). Cache Components shipped as an opt-in flag in Next.js 16.0 and is the model this article teaches; where the still-supported previous model (implicit &lt;code&gt;fetch&lt;/code&gt; caching, route segment configs like &lt;code&gt;export const dynamic&lt;/code&gt;) differs, it's called out explicitly rather than left implied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The problem: one cookie read, one fully dynamic page&lt;/li&gt;
&lt;li&gt;The mental model: pieces decide, not routes&lt;/li&gt;
&lt;li&gt;Stage 1: turning it on&lt;/li&gt;
&lt;li&gt;Stage 2: caching data with &lt;code&gt;use cache&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Stage 3: caching a component, and reading &lt;code&gt;cacheLife&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Stage 4: the part that can't be cached — Suspense&lt;/li&gt;
&lt;li&gt;Stage 5: invalidating on demand with &lt;code&gt;cacheTag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Edge cases and gotchas&lt;/li&gt;
&lt;li&gt;Best practices&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;Cheat sheet&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem: one cookie read, one fully dynamic page
&lt;/h2&gt;

&lt;p&gt;Here's a blog layout that reads a saved theme preference so it can render the right class on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx — pre-Cache-Components App Router&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cookies()&lt;/code&gt; is a &lt;em&gt;request-time&lt;/em&gt; API — it can only produce a value once an actual request exists, so there's no way to know it at build time. In the App Router's previous rendering model, that fact wasn't scoped to the component that called it: reading a dynamic API anywhere in a route's component tree opted the &lt;strong&gt;entire route&lt;/strong&gt; out of static rendering. The header, the article body, the footer, the "10 related posts" list that's the same for every visitor — all of it now re-renders on the server, on every request, because one &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; class needed to know something about the current user.&lt;/p&gt;

&lt;p&gt;You can work around this in the previous model (extract the theme read into a small Client Component that reads &lt;code&gt;document.cookie&lt;/code&gt; after hydration, for instance), but the workaround is the tell: the framework's default behavior didn't distinguish "this one value needs live data" from "this route needs live data." Cache Components draws that line at the component, not the route.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: pieces decide, not routes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The mental model:&lt;/strong&gt; in the previous model, a &lt;em&gt;route&lt;/em&gt; got one verdict — static or dynamic — decided by the most demanding thing anywhere in its tree. With Cache Components enabled, that verdict moves down to individual functions and components. Each one is either cached (with an explicit lifetime), streamed behind a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary, or — if it does neither and touches something request-specific — flagged by the framework as needing one of those two treatments before the build will pass.&lt;/p&gt;

&lt;p&gt;Next.js still produces one artifact per route: a &lt;strong&gt;static shell&lt;/strong&gt;, prerendered at build time, containing every static and cached piece plus fallback UI for anything still streaming. That shell is what a CDN can serve instantly on a direct visit. The pieces behind &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; fill in afterward, at request time, without dragging the rest of the page down with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: turning it on
&lt;/h2&gt;

&lt;p&gt;Cache Components is an opt-in flag as of Next.js 16.0 — a fresh &lt;code&gt;create-next-app&lt;/code&gt; project doesn't enable it by default yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; this one flag replaces three separate experimental flags from Next.js 15 (&lt;code&gt;dynamicIO&lt;/code&gt;, &lt;code&gt;useCache&lt;/code&gt;, and &lt;code&gt;ppr&lt;/code&gt;) with one unified setting, and it requires the Node.js runtime — routes still exporting the deprecated &lt;code&gt;runtime = 'edge'&lt;/code&gt; need to migrate first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: caching data with &lt;code&gt;use cache&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;use cache&lt;/code&gt; directive marks an async function's or component's return value as cacheable. Start with a plain data-fetching function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/lib/posts.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRecentPosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hours&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call with a given set of inputs runs the function and stores the result; every later call with the &lt;em&gt;same&lt;/em&gt; inputs — including different requests, from different visitors — reuses it, until the lifetime you set with &lt;code&gt;cacheLife&lt;/code&gt; expires. Arguments and any variables captured from an outer scope become part of the cache key automatically, so &lt;code&gt;getRecentPosts(category)&lt;/code&gt; called with two different categories gets two separate cache entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; &lt;code&gt;use cache&lt;/code&gt; caches a &lt;em&gt;result&lt;/em&gt;, keyed by its inputs — not a route, not a URL. That's what lets a component ten levels deep cache independently from everything around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: caching a component, and reading &lt;code&gt;cacheLife&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The same directive works at the component level, caching everything the component renders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/blog/recent-posts.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RecentPosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hours&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cacheLife('hours')&lt;/code&gt; isn't a made-up duration — it's one of six built-in profiles, each balancing three numbers: how long the &lt;em&gt;client&lt;/em&gt; trusts a cached copy without checking (&lt;code&gt;stale&lt;/code&gt;), how often the &lt;em&gt;server&lt;/em&gt; regenerates it in the background (&lt;code&gt;revalidate&lt;/code&gt;), and when it's dropped entirely if nobody's asked for it (&lt;code&gt;expire&lt;/code&gt;). Omit &lt;code&gt;cacheLife&lt;/code&gt; and the &lt;code&gt;default&lt;/code&gt; profile applies implicitly — which works, but leaves the lifetime invisible at the call site. Naming it explicitly is the recommended habit.&lt;/p&gt;

&lt;p&gt;If this result is part of what could go into the route's prerendered static shell (its &lt;code&gt;stale&lt;/code&gt; window is long enough), it's filled in at build time and served straight from a CDN on a direct visit — no server round trip at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: the part that can't be cached — Suspense
&lt;/h2&gt;

&lt;p&gt;Back to the theme example. The fix isn't to avoid &lt;code&gt;cookies()&lt;/code&gt; — it's to contain it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx — with Cache Components&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeBody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ThemeBody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeBody&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RootLayout&lt;/code&gt; itself no longer awaits &lt;code&gt;cookies()&lt;/code&gt;, so it isn't request-dependent, and it completes during prerendering. &lt;code&gt;ThemeBody&lt;/code&gt; is the only thing that streams in at request time — the header, the article body, and everything else in &lt;code&gt;children&lt;/code&gt; that's cached or static ships in the initial shell exactly as before. One component's need for live data no longer taxes the whole page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; doesn't make a component dynamic — it gives a component that's &lt;em&gt;already&lt;/em&gt; dynamic (because it reads a runtime API, or fetches without caching) somewhere to put its fallback so the rest of the shell doesn't have to wait for it. A component that only does synchronous work completes during prerendering regardless of whether it's wrapped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: invalidating on demand with &lt;code&gt;cacheTag&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Time-based expiry (&lt;code&gt;cacheLife&lt;/code&gt;) and on-demand invalidation (&lt;code&gt;cacheTag&lt;/code&gt;) aren't alternatives — they're usually paired. &lt;code&gt;RecentPosts&lt;/code&gt; above tagged its cache entry &lt;code&gt;'posts'&lt;/code&gt;. When a new post is published, invalidate every entry with that tag, from anywhere the mutation happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;publishPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;updateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// every 'posts'-tagged cache entry is now stale&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same pattern as the older &lt;code&gt;revalidateTag&lt;/code&gt;, but &lt;code&gt;updateTag&lt;/code&gt; is aware of Cache Components' server and client caches together, so tagging and invalidating stays a single mental step regardless of which layer actually stored the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/nextjs-weekly-cache-components-explained/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A cached scope can't read runtime APIs at all.&lt;/strong&gt; Calling &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or reading &lt;code&gt;searchParams&lt;/code&gt; directly inside a &lt;code&gt;use cache&lt;/code&gt; function — or inside anything it calls — throws. Read the value in an uncached component first, then pass it as an argument to the cached function; the argument becomes part of the cache key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passing an uncached promise into a cached function hangs the build.&lt;/strong&gt; If a &lt;code&gt;use cache&lt;/code&gt; function awaits a promise that resolves to request-specific or otherwise-uncached data (received as a prop, from a closure, or from shared storage like a &lt;code&gt;Map&lt;/code&gt;), the build waits for data that can never resolve during prerendering and times out after 50 seconds. Await the value outside the cached scope and pass the resolved value in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Math.random()&lt;/code&gt;, &lt;code&gt;Date.now()&lt;/code&gt;, and &lt;code&gt;crypto.randomUUID()&lt;/code&gt; need an explicit choice.&lt;/strong&gt; They produce a different value every call, so Cache Components requires you to say what you mean: call &lt;code&gt;connection()&lt;/code&gt; before them and wrap in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; to get a genuinely unique value per request, or wrap them in &lt;code&gt;use cache&lt;/code&gt; so every visitor sees the same value until it revalidates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serialization has real limits.&lt;/strong&gt; Arguments and cached return values must be serializable — primitives, plain objects, arrays, &lt;code&gt;Date&lt;/code&gt;/&lt;code&gt;Map&lt;/code&gt;/&lt;code&gt;Set&lt;/code&gt;, and (for return values only) JSX. Class instances, functions, and &lt;code&gt;URL&lt;/code&gt; instances aren't allowed, except as opaque pass-through props like &lt;code&gt;children&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft Mode bypasses the cache entirely.&lt;/strong&gt; With Draft Mode enabled, every cached function re-executes on every request and nothing is written to the cache — by design, so preview content is never stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bots and crawlers skip the shell.&lt;/strong&gt; Because they need a complete document, Next.js detects them by user agent and renders the whole page dynamically at request time instead of serving the static shell. If any part of your shell depends on build-time-only data, make sure the same data is reachable at request time too, or a page that renders for a person can fail for a crawler.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push runtime API reads as deep into the tree as they'll go.&lt;/strong&gt; A &lt;code&gt;params&lt;/code&gt; or &lt;code&gt;cookies()&lt;/code&gt; read at the top of a layout blocks everything below it from being static; the same read three components down blocks only that subtree. The deeper the dynamic work sits, the more of the page prerenders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pair every &lt;code&gt;use cache&lt;/code&gt; with an explicit &lt;code&gt;cacheLife&lt;/code&gt;.&lt;/strong&gt; The implicit &lt;code&gt;default&lt;/code&gt; profile (5-minute stale, 15-minute revalidate, never expires) works, but naming the profile you actually mean documents the decision at the call site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach for &lt;code&gt;cacheTag&lt;/code&gt; + &lt;code&gt;updateTag&lt;/code&gt; for anything invalidated by a mutation&lt;/strong&gt;, and a longer &lt;code&gt;cacheLife&lt;/code&gt; (&lt;code&gt;days&lt;/code&gt;, &lt;code&gt;weeks&lt;/code&gt;, or &lt;code&gt;max&lt;/code&gt;) for content that only changes when someone edits it — the two together mean you rarely need a short polling-style lifetime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't cache what should stream.&lt;/strong&gt; A component that genuinely needs the current request — a cart total from a session cookie, a personalized recommendation — belongs behind &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, not squeezed into a cache with an artificially short lifetime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;use cache: private&lt;/code&gt; is the exception, not the default.&lt;/strong&gt; It exists for cases where you can't refactor to pass runtime data as arguments; reach for it rarely, since regular &lt;code&gt;use cache&lt;/code&gt; plus an extracted argument covers most real cases and stays easier to reason about.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I have to enable &lt;code&gt;cacheComponents&lt;/code&gt; to use Next.js 16?
&lt;/h3&gt;

&lt;p&gt;No. It's an opt-in flag. Without it, your app uses the previous rendering model — &lt;code&gt;fetch&lt;/code&gt; requests are uncached by default (a change from Next.js 14), and route segment configs like &lt;code&gt;export const dynamic&lt;/code&gt; and &lt;code&gt;revalidate&lt;/code&gt; still work exactly as before.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Cache Components replace &lt;code&gt;revalidatePath&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Not entirely — &lt;code&gt;revalidatePath&lt;/code&gt; still exists for the previous model's route-level cache. Inside Cache Components, prefer &lt;code&gt;cacheTag&lt;/code&gt; plus &lt;code&gt;updateTag&lt;/code&gt; (or &lt;code&gt;revalidateTag&lt;/code&gt;), which target specific cached results by tag rather than an entire route.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Cache Components the same thing as Partial Prerendering (PPR)?
&lt;/h3&gt;

&lt;p&gt;Related, not identical. Partial Prerendering — a static shell plus streaming holes — is the &lt;em&gt;rendering&lt;/em&gt; behavior Cache Components implements by default. &lt;code&gt;cacheComponents: true&lt;/code&gt; is the single flag that turns PPR on along with &lt;code&gt;use cache&lt;/code&gt; and the removal of implicit dynamic-API-triggers-whole-route behavior; you no longer set an experimental PPR flag separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;use cache&lt;/code&gt; persist across deployments?
&lt;/h3&gt;

&lt;p&gt;No. Every cache key includes the build ID (or your configured &lt;code&gt;deploymentId&lt;/code&gt;), so a new deploy starts with an empty cache, even for the durable &lt;code&gt;use cache: remote&lt;/code&gt; variant. That's deliberate — it guarantees a deploy never serves output built from stale code.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I forget &lt;code&gt;cacheLife&lt;/code&gt; on a &lt;code&gt;use cache&lt;/code&gt; function?
&lt;/h3&gt;

&lt;p&gt;Nothing breaks — the &lt;code&gt;default&lt;/code&gt; profile applies (5-minute client-side stale window, 15-minute server-side revalidate, no time-based expiry). The framework recommends setting it explicitly anyway, since the alternative is a lifetime that's easy to lose track of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Enable Cache Components&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cacheComponents: true&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Requires Node.js runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache a data function&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'use cache'&lt;/code&gt; at the top of an async function&lt;/td&gt;
&lt;td&gt;Result keyed by arguments + captured closures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache a whole component&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'use cache'&lt;/code&gt; at the top of an async component&lt;/td&gt;
&lt;td&gt;Composed &lt;code&gt;children&lt;/code&gt;/slots pass through uncached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache every export in a file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'use cache'&lt;/code&gt; at the top of the file&lt;/td&gt;
&lt;td&gt;Every exported function must be async&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set an explicit lifetime&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cacheLife('hours')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One call per function invocation, inside the cached scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tag a cache entry&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cacheTag('posts')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pairs with &lt;code&gt;updateTag&lt;/code&gt;/&lt;code&gt;revalidateTag&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalidate on demand&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;updateTag('posts')&lt;/code&gt; inside a Server Action&lt;/td&gt;
&lt;td&gt;Invalidates every entry with that tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream request-specific data&lt;/td&gt;
&lt;td&gt;Wrap in &lt;code&gt;&amp;lt;Suspense fallback={...}&amp;gt;&lt;/code&gt;, read &lt;code&gt;cookies()&lt;/code&gt;/&lt;code&gt;headers()&lt;/code&gt; inside&lt;/td&gt;
&lt;td&gt;Fallback ships in the static shell; content streams at request time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get a unique value per request&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;await connection()&lt;/code&gt; then &lt;code&gt;Math.random()&lt;/code&gt;/&lt;code&gt;Date.now()&lt;/code&gt;, inside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Forces request-time evaluation instead of a cached build-time value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The pattern in one page: static, cached, and streaming together&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BlogPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Static — prerendered automatically&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RecentPosts&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* cached, joins the static shell */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading your preferences…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserPreferences&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* streams in at request time */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RecentPosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hours&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserPreferences&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;aside&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Theme: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;aside&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The previous App Router model gave each &lt;em&gt;route&lt;/em&gt; one verdict — static or dynamic — decided by its most demanding component. Cache Components moves that decision down to individual functions and components.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;use cache&lt;/code&gt; caches a result by its inputs; pair it with an explicit &lt;code&gt;cacheLife&lt;/code&gt; so the lifetime is visible at the call site, not implicit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; is how a genuinely request-specific piece streams in without dragging the rest of the page's caching down with it — it doesn't make a component dynamic, it gives one that already is somewhere to put its fallback.&lt;/li&gt;
&lt;li&gt;Time-based (&lt;code&gt;cacheLife&lt;/code&gt;) and on-demand (&lt;code&gt;cacheTag&lt;/code&gt; + &lt;code&gt;updateTag&lt;/code&gt;) revalidation are complementary, not competing — most real content wants both.&lt;/li&gt;
&lt;li&gt;The line between "this needs live data" and "this route needs live data" is now drawn at the component, and that's the whole point.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Back to that layout
&lt;/h2&gt;

&lt;p&gt;The fix for the theme cookie wasn't to stop reading it — it was to stop letting one read decide the fate of everything around it. Move the read into its own component, wrap it in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;, and the header, the article, and the "related posts" list go back to shipping from the edge, instantly, while the one thing that actually needed to know who's visiting still gets to ask.&lt;/p&gt;

&lt;p&gt;If you want the background this article assumes — what a Server Component actually is and why "it's just SSR" is the wrong mental model — &lt;a href="https://dev.to/parsajiravand/server-components-without-the-hype-a-mental-model-that-sticks-4k5e"&gt;Server Components Without the Hype&lt;/a&gt; covers that half, and this article picks up from there.&lt;/p&gt;

&lt;p&gt;What's the last route you had to manually pull out of "fully dynamic" — and did you know at the time which single line caused it?&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/nextjs-weekly-cache-components-explained/quiz" rel="noopener noreferrer"&gt;Take the 8-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your Router Doesn't Intercept Navigation. It Reacts To It.</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:23:43 +0000</pubDate>
      <link>https://dev.to/parsajiravand/your-router-doesnt-intercept-navigation-it-reacts-to-it-c5l</link>
      <guid>https://dev.to/parsajiravand/your-router-doesnt-intercept-navigation-it-reacts-to-it-c5l</guid>
      <description>&lt;p&gt;Click a link. Before the new page finishes loading, click a different one. You'd expect the second click to win — that's what happens with a plain &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; and a full page load. In a lot of single-page apps, it doesn't.&lt;/p&gt;

&lt;p&gt;What actually happens: the first navigation starts fetching data. The second navigation starts too. Whichever fetch resolves last wins, regardless of which link you clicked last. For a second, the URL bar says one page and the rendered content says another. On a slow connection, "for a second" can be long enough for someone to screenshot it and file a bug titled "app shows wrong page."&lt;/p&gt;

&lt;p&gt;Here's the part that surprises people: this isn't a bug in your router. It's the shape of the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;pushState&lt;/code&gt; routing actually works
&lt;/h2&gt;

&lt;p&gt;Every client-side router — the ones you've built by hand and the ones bundled into frameworks — is doing roughly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;renderRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fetch data, swap the view&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;popstate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// back/forward button&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that again and notice what's missing: nothing here knows about the &lt;em&gt;previous&lt;/em&gt; &lt;code&gt;renderRoute&lt;/code&gt; call while a new one starts. &lt;code&gt;pushState&lt;/code&gt; already changed the URL by the time &lt;code&gt;renderRoute&lt;/code&gt; begins — the navigation, as far as the browser's history is concerned, already happened. Your router isn't intercepting it. It's reacting to something already committed, then racing its own async work against whatever the last reaction was doing.&lt;/p&gt;

&lt;p&gt;That's the double-click bug in one sentence: two &lt;code&gt;renderRoute&lt;/code&gt; calls, no relationship between them, last one to finish wins. Add a canceled &lt;code&gt;fetch&lt;/code&gt; and an &lt;code&gt;AbortController&lt;/code&gt; to the mix and you can make it &lt;em&gt;mostly&lt;/em&gt; go away — mature router libraries do exactly that. But you're patching a race condition from outside the event that caused it, because the click handler and the URL change were never one atomic thing to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API built to fire before the URL changes
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API" rel="noopener noreferrer"&gt;Navigation API&lt;/a&gt; exists specifically to close that gap. Instead of listening for &lt;code&gt;popstate&lt;/code&gt; after a change, you listen for &lt;code&gt;navigate&lt;/code&gt; on &lt;code&gt;window.navigation&lt;/code&gt; — and that event fires &lt;em&gt;before&lt;/em&gt; the browser commits to the new URL, for every kind of navigation: link clicks, back/forward, &lt;code&gt;history.pushState&lt;/code&gt;, even form submissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navigate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canIntercept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// e.g. cross-origin navigations can't be&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the browser waits on this&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;intercept()&lt;/code&gt; is the piece &lt;code&gt;popstate&lt;/code&gt; never had: it hands the browser a promise and the browser &lt;em&gt;waits on it&lt;/em&gt; before treating the navigation as finished — no spinner state you have to fake, no separate "is this route still current" flag to check by hand. And because every &lt;code&gt;navigate&lt;/code&gt; event carries its own &lt;code&gt;event.destination&lt;/code&gt;, a second click firing a second &lt;code&gt;navigate&lt;/code&gt; event doesn't need to guess whether an earlier one is still in flight — you can call &lt;code&gt;event.signal&lt;/code&gt; (an &lt;code&gt;AbortSignal&lt;/code&gt; the browser aborts automatically when a newer navigation supersedes this one) inside your handler and pass it straight to &lt;code&gt;fetch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click a second link before the first &lt;code&gt;handler&lt;/code&gt; resolves, and the browser aborts the first navigation's signal for you. The stale fetch throws, you skip rendering it, and there's no manual bookkeeping to get wrong — the race condition isn't patched, it's structurally not possible to hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get that &lt;code&gt;pushState&lt;/code&gt; never gave you
&lt;/h2&gt;

&lt;p&gt;A few things fall out of treating navigation as one interceptable event instead of a click handler plus a &lt;code&gt;popstate&lt;/code&gt; listener bolted on the side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One event for everything.&lt;/strong&gt; Link clicks, back/forward, and programmatic &lt;code&gt;navigation.navigate()&lt;/code&gt; calls all go through the same &lt;code&gt;navigate&lt;/code&gt; event — you're not maintaining a click handler and a separate &lt;code&gt;popstate&lt;/code&gt; handler that have to agree with each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancellation that's actually cancellation.&lt;/strong&gt; &lt;code&gt;event.preventDefault()&lt;/code&gt; on &lt;code&gt;navigate&lt;/code&gt; stops the navigation before the URL changes, not after — useful for "you have unsaved changes" guards that used to require faking the URL back with another &lt;code&gt;pushState&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real navigation state.&lt;/strong&gt; &lt;code&gt;navigation.currentEntry&lt;/code&gt;, &lt;code&gt;navigation.entries()&lt;/code&gt;, and events like &lt;code&gt;navigatesuccess&lt;/code&gt; / &lt;code&gt;navigateerror&lt;/code&gt; give you a queryable list of history entries with keys and state, instead of &lt;code&gt;history.length&lt;/code&gt; (a number that tells you nothing about &lt;em&gt;what's&lt;/em&gt; in the stack).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this replaces routing logic — you still decide what URL maps to what view. It replaces the part where the browser's actual navigation and your app's idea of navigation are two loosely-synced systems held together by &lt;code&gt;preventDefault&lt;/code&gt; and hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands today
&lt;/h2&gt;

&lt;p&gt;The Navigation API has shipped in Chrome and Edge since 2022. Safari and Firefox don't have it yet as of this writing, which is the honest caveat: this isn't a drop-in replacement for your router today, and any production use needs a fallback path (or a router library that already detects and uses it under the hood, falling back to &lt;code&gt;pushState&lt;/code&gt; where it's missing). Check current support before reaching for it directly in anything beyond a Chromium-only tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/navigation-api-intercept-navigation/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing worth remembering
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;popstate&lt;/code&gt; tells you a navigation happened. &lt;code&gt;navigate&lt;/code&gt; tells you one is &lt;em&gt;about&lt;/em&gt; to, and lets you decide what "happened" even means. That difference is the whole reason the double-click race exists in the first place — and the whole reason it stops existing once the browser is holding the promise instead of you holding a flag.&lt;/p&gt;

&lt;p&gt;Next time your router's navigation logic starts growing &lt;code&gt;isNavigating&lt;/code&gt; booleans and manual &lt;code&gt;AbortController&lt;/code&gt; bookkeeping, that's usually this gap showing through. Have you hit the double-navigation race in production, or did your router already paper over it? I'd like to know which libraries got this right early.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/navigation-api-intercept-navigation/quiz" rel="noopener noreferrer"&gt;Take the 8-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>browser</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Debounce and Throttle in JavaScript: The Complete Guide</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:12:13 +0000</pubDate>
      <link>https://dev.to/parsajiravand/debounce-and-throttle-in-javascript-the-complete-guide-2o61</link>
      <guid>https://dev.to/parsajiravand/debounce-and-throttle-in-javascript-the-complete-guide-2o61</guid>
      <description>&lt;p&gt;Type "javascript" into an unthrottled search box and watch the network tab: an HTTP request for &lt;code&gt;j&lt;/code&gt;, another for &lt;code&gt;ja&lt;/code&gt;, another for &lt;code&gt;jav&lt;/code&gt;, ten in total, nine of them thrown away before the response even lands. The server did ten times the work the feature needed, and the UI flickers with results for a query the user abandoned two keystrokes ago. The fix is two small, deceptively simple functions — debounce and throttle — and the part that trips people up isn't writing them, it's knowing exactly when each one fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll learn
&lt;/h2&gt;

&lt;p&gt;By the end of this guide you'll be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explain the precise difference between debounce and throttle, not just "they both slow things down"&lt;/li&gt;
&lt;li&gt;Write a correct debounce and a correct throttle from scratch, including leading/trailing edge behavior&lt;/li&gt;
&lt;li&gt;Choose the right one for search inputs, scroll handlers, resize handlers, and button clicks&lt;/li&gt;
&lt;li&gt;Avoid the memory leaks and stale-closure bugs both patterns cause in React and vanilla JS alike&lt;/li&gt;
&lt;li&gt;Use a copy-paste utility with &lt;code&gt;cancel()&lt;/code&gt; and &lt;code&gt;flush()&lt;/code&gt; support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; you write JavaScript day to day, you've attached an event listener before, and you've either hand-rolled a &lt;code&gt;setTimeout&lt;/code&gt; hack for this exact problem or reached for lodash without fully trusting what its defaults do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why debounce and throttle exist&lt;/li&gt;
&lt;li&gt;The mental model: a bouncer, not a filter&lt;/li&gt;
&lt;li&gt;Stage 1: debounce from scratch&lt;/li&gt;
&lt;li&gt;Stage 2: throttle from scratch&lt;/li&gt;
&lt;li&gt;Stage 3: leading and trailing edges&lt;/li&gt;
&lt;li&gt;Stage 4: cancel, flush, and cleanup&lt;/li&gt;
&lt;li&gt;Stage 5: using them in React&lt;/li&gt;
&lt;li&gt;Edge cases and gotchas&lt;/li&gt;
&lt;li&gt;Best practices&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;Cheat sheet&lt;/li&gt;
&lt;li&gt;Key takeaways&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why debounce and throttle exist
&lt;/h2&gt;

&lt;p&gt;Here's the naive version of a live search box — the one almost everyone writes first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the wrong way — fires a request on every single keystroke&lt;/span&gt;
&lt;span class="nx"&gt;searchInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// one HTTP request per keystroke&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type a six-letter word at a normal pace and this fires six requests in well under a second, five of which are already obsolete by the time their responses arrive. Worse, network responses don't always resolve in the order they were sent — a slow response for &lt;code&gt;jav&lt;/code&gt; can arrive &lt;em&gt;after&lt;/em&gt; the fast response for &lt;code&gt;javascript&lt;/code&gt;, and now the screen is showing stale results for a query the user already replaced. The bug isn't visible in a quick manual test because your laptop and the API are both fast; it shows up for a real user on a real network, and by then it looks like "search is flaky" rather than "search fires way too often."&lt;/p&gt;

&lt;p&gt;The same shape of problem hits scroll and resize handlers, just with a different failure mode. A &lt;code&gt;scroll&lt;/code&gt; event can fire dozens of times per second. If the handler does anything nontrivial — reading &lt;code&gt;getBoundingClientRect()&lt;/code&gt;, updating layout, running a chunk of business logic — the page starts dropping frames and scrolling turns jerky, even though nothing is technically "broken."&lt;/p&gt;

&lt;p&gt;Both problems come from the same root cause: &lt;strong&gt;the event source fires far more often than the response actually needs to run.&lt;/strong&gt; Debounce and throttle are two different answers to "how often is often enough," and they are not interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: a bouncer, not a filter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The mental model:&lt;/strong&gt; neither function &lt;em&gt;filters&lt;/em&gt; events — every event still reaches your wrapper and every event still runs a check. What changes is how often the check lets the real work through, and the two functions use opposite strategies for deciding that.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debounce says "wait for quiet."&lt;/strong&gt; Every call resets a timer. The wrapped function only runs once the calls actually &lt;em&gt;stop&lt;/em&gt; for the configured delay. Think of an elevator door: every time someone walks up, the door resets its close timer. It only closes once nobody has approached it for a few seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throttle says "at most once per interval."&lt;/strong&gt; It doesn't care whether calls are still coming in — it just refuses to let the wrapped function run again until a fixed amount of time has passed since the last time it ran. Think of a metronome, or a bouncer who lets one person through the door every two seconds regardless of how long the line is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single distinction — "wait for silence" versus "space it out at a fixed rate" — explains almost every behavior difference in the rest of this guide. Debounce is right when you only care about the &lt;em&gt;final&lt;/em&gt; state (the finished search query). Throttle is right when you need &lt;em&gt;regular updates during&lt;/em&gt; continuous activity (a scroll position that should keep updating while the user scrolls).&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: debounce from scratch
&lt;/h2&gt;

&lt;p&gt;The smallest correct debounce is short — a closure holding one timer ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// lives across calls thanks to the closure&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// cancel whatever was pending&lt;/span&gt;
    &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;searchInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;debouncedSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; every call to &lt;code&gt;debounced&lt;/code&gt; cancels the previous pending timer and starts a new one. &lt;code&gt;fn&lt;/code&gt; only ever actually runs if 300ms pass with &lt;em&gt;no&lt;/em&gt; new call in between — which is exactly "wait for quiet," implemented as literally as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Type "js" quickly and &lt;code&gt;debounced&lt;/code&gt; runs twice (once per keystroke) but &lt;code&gt;fn&lt;/code&gt; runs zero times until you stop — then it runs exactly once, 300ms after your last keystroke, with the final value of &lt;code&gt;query&lt;/code&gt;. That's the whole mechanism. Everything else in this guide is a variation on this six-line function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: throttle from scratch
&lt;/h2&gt;

&lt;p&gt;Throttle needs to track &lt;em&gt;when it last ran&lt;/em&gt;, not whether a timer is pending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// timestamp of the last time fn actually executed&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;throttled&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;throttledOnScroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;updateScrollProgress&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;throttledOnScroll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; this implementation runs &lt;code&gt;fn&lt;/code&gt; immediately on the very first call (because &lt;code&gt;now - lastRun&lt;/code&gt; starts effectively infinite), then ignores every call until &lt;code&gt;intervalMs&lt;/code&gt; has elapsed, at which point the next call through gets to run. Calls that arrive during the "cooldown" are dropped entirely — not queued, not delayed, just discarded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last detail matters: with this specific implementation, if the burst of calls stops &lt;em&gt;during&lt;/em&gt; a cooldown window, the very last call in the burst is simply lost — &lt;code&gt;fn&lt;/code&gt; doesn't get one final run with the latest arguments. Stage 3 fixes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: leading and trailing edges
&lt;/h2&gt;

&lt;p&gt;"Leading edge" means running on the &lt;em&gt;first&lt;/em&gt; call in a burst; "trailing edge" means running once more after the burst ends, with the latest arguments. The debounce in Stage 1 is trailing-only. The throttle in Stage 2 is leading-only. A production-grade version usually supports both, because dropping the trailing call (throttle) or delaying every call including the first one (debounce) is sometimes the wrong tradeoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;leading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trailing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;throttled&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastRun&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;lastArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leading&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trailing&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastArgs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; &lt;code&gt;leading&lt;/code&gt; controls whether the very first call in a burst runs immediately; &lt;code&gt;trailing&lt;/code&gt; controls whether one extra call fires after the burst goes quiet, using whatever arguments arrived last. lodash's &lt;code&gt;_.throttle&lt;/code&gt; defaults to &lt;code&gt;{ leading: true, trailing: true }&lt;/code&gt;, and its &lt;code&gt;_.debounce&lt;/code&gt; defaults to &lt;code&gt;{ leading: false, trailing: true }&lt;/code&gt; — which is exactly why debounce "feels like" it only fires at the end, while throttle "feels like" it fires immediately and then periodically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Stage 4: cancel, flush, and cleanup
&lt;/h2&gt;

&lt;p&gt;A debounce or throttle you can't cancel is a liability the moment its owner disappears — a component unmounts, a modal closes, a request is superseded. Attach the controls directly to the returned function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// drop the pending call&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedSave&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saveDraft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;debouncedSave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;debouncedSave&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "draft text" will never be saved&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flush()&lt;/code&gt; is the mirror image: run the pending call &lt;em&gt;right now&lt;/em&gt; instead of waiting or dropping it — useful when the user explicitly submits a form while a debounced autosave is still pending, so the two writes don't race.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: using them in React
&lt;/h2&gt;

&lt;p&gt;The trap in React isn't the debounce function itself — it's &lt;em&gt;where&lt;/em&gt; you create it. Creating a new debounced function on every render breaks the whole mechanism, because each render's closure has no memory of the previous render's timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the wrong way — a brand-new debounce (and a brand-new timer) every render&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchBox&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// recreated every render&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;debouncedSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// this instance's timer never gets to fire before a new one replaces it&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the debounced function once, with &lt;code&gt;useMemo&lt;/code&gt; or &lt;code&gt;useRef&lt;/code&gt;, and cancel it on unmount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchBox&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debouncedSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;debouncedSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// no fetch after this component is gone&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;debouncedSearch&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;debouncedSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key concept:&lt;/strong&gt; the debounced function must outlive individual renders (created once, stored in a ref or memoized with an empty dependency array) and must be explicitly cancelled in a cleanup function, or its pending timer will call &lt;code&gt;fetchResults&lt;/code&gt; on state that no longer exists.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Edge cases and gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stale closures.&lt;/strong&gt; If &lt;code&gt;fn&lt;/code&gt; inside a debounce/throttle closes over a variable that changes between calls (a piece of state, a prop), the call that eventually fires can use an outdated value. &lt;code&gt;useCallback&lt;/code&gt;/&lt;code&gt;useRef&lt;/code&gt; patterns exist specifically to solve this in React; in vanilla JS, pass the current value as an argument rather than relying on the closure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;this&lt;/code&gt; binding.&lt;/strong&gt; The hand-written implementations above use &lt;code&gt;fn.apply(this, args)&lt;/code&gt; so a debounced/throttled &lt;em&gt;method&lt;/em&gt; still sees the right &lt;code&gt;this&lt;/code&gt;. If you strip that out, calling &lt;code&gt;debounce(obj.method, 300)()&lt;/code&gt; silently breaks &lt;code&gt;this&lt;/code&gt; inside &lt;code&gt;method&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debouncing an &lt;code&gt;async&lt;/code&gt; function doesn't cancel in-flight work.&lt;/strong&gt; Debounce delays &lt;em&gt;calling&lt;/em&gt; the function; it does nothing about a &lt;code&gt;fetch&lt;/code&gt; that's already in progress from a previous call. Pair debounce with an &lt;code&gt;AbortController&lt;/code&gt; if a slow, superseded request could still resolve and overwrite a newer one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race conditions between the trailing call and unmount.&lt;/strong&gt; A debounce's &lt;code&gt;setTimeout&lt;/code&gt; keeps a reference to &lt;code&gt;fn&lt;/code&gt; alive even after the component that created it is gone. Without &lt;code&gt;cancel()&lt;/code&gt; on cleanup, the trailing call still fires and can throw ("cannot update state on an unmounted component") or write to a resource that no longer applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throttle intervals and animation.&lt;/strong&gt; A &lt;code&gt;scroll&lt;/code&gt; or &lt;code&gt;mousemove&lt;/code&gt; throttle set to a fixed millisecond interval can visibly stutter, because it's not synchronized with the browser's paint cycle. For anything visual, prefer &lt;code&gt;requestAnimationFrame&lt;/code&gt;-based throttling: run at most once per frame instead of once per N milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing.&lt;/strong&gt; Both patterns depend on real time passing, which makes tests flaky if you &lt;code&gt;sleep&lt;/code&gt;. Use fake timers (&lt;code&gt;jest.useFakeTimers()&lt;/code&gt; / &lt;code&gt;vi.useFakeTimers()&lt;/code&gt;) and advance them explicitly (&lt;code&gt;jest.advanceTimersByTime(300)&lt;/code&gt;) instead of waiting on the wall clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debounce delay versus perceived responsiveness.&lt;/strong&gt; A 300ms debounce feels instant to most users; anything above ~500ms on a search-as-you-type field starts to feel sluggish, because the user has already mentally "sent" the query.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best practices (when (not) to use them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reach for debounce when&lt;/strong&gt; you only care about the value once activity settles: search-as-you-type, autosave, form validation that shouldn't run on every keystroke, resize-triggered layout recalculation where only the final size matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reach for throttle when&lt;/strong&gt; you need periodic updates &lt;em&gt;during&lt;/em&gt; continuous activity, not just at the end: scroll-position tracking, a progress indicator following &lt;code&gt;mousemove&lt;/code&gt;, rate-limiting how often a "user is typing" indicator pings a server, infinite-scroll trigger checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid both when&lt;/strong&gt; the action needs to feel instantaneous every single time — a button click, an "add to cart," a keyboard shortcut. Delaying or dropping those erodes trust in the UI even if it's technically more "efficient." If a click handler is slow, fix the handler; don't debounce the click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't stack them by accident.&lt;/strong&gt; Wrapping an already-throttled handler in another library's debounce (or vice versa) is a common cause of "my scroll handler feels randomly laggy" bugs — pick one strategy per event source and be deliberate about the delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What's the actual difference between debounce and throttle?
&lt;/h3&gt;

&lt;p&gt;Debounce waits for a pause in activity and then runs once; throttle runs at a fixed maximum rate regardless of whether activity is still ongoing. Debounce answers "what's the final state?"; throttle answers "give me periodic updates while this keeps happening."&lt;/p&gt;

&lt;h3&gt;
  
  
  Does lodash's &lt;code&gt;_.debounce&lt;/code&gt; behave differently from a hand-rolled one?
&lt;/h3&gt;

&lt;p&gt;Functionally, a correct hand-rolled trailing-edge debounce matches &lt;code&gt;_.debounce&lt;/code&gt;'s default behavior (&lt;code&gt;leading: false, trailing: true&lt;/code&gt;). lodash additionally ships &lt;code&gt;cancel()&lt;/code&gt;, &lt;code&gt;flush()&lt;/code&gt;, and a &lt;code&gt;maxWait&lt;/code&gt; option (a ceiling on how long calls can be delayed even under continuous activity) — genuinely useful extras, not a different core algorithm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I debounce an async function?
&lt;/h3&gt;

&lt;p&gt;Yes, but debounce only controls &lt;em&gt;when the call happens&lt;/em&gt; — it has no knowledge of what the async function does afterward. If an earlier (superseded) call's promise resolves after a later one, you can still get out-of-order results unless you also track "is this the latest call" or cancel the earlier request with an &lt;code&gt;AbortController&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use &lt;code&gt;requestAnimationFrame&lt;/code&gt; instead of throttle for scroll or resize?
&lt;/h3&gt;

&lt;p&gt;For anything that updates visuals (position, size, opacity), yes — &lt;code&gt;requestAnimationFrame&lt;/code&gt; throttling caps the work at once per paint, which is both smoother and never more work than the browser can actually display. Millisecond-based throttling is still the right tool for non-visual rate-limiting, like capping how often you ping an analytics endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I cancel a debounced or throttled call?
&lt;/h3&gt;

&lt;p&gt;Attach a &lt;code&gt;.cancel()&lt;/code&gt; method to the returned function (Stage 4) and call it — in &lt;code&gt;setTimeout&lt;/code&gt;'s case that's a &lt;code&gt;clearTimeout&lt;/code&gt;; for lodash, &lt;code&gt;_.debounce&lt;/code&gt; and &lt;code&gt;_.throttle&lt;/code&gt; both return functions with a built-in &lt;code&gt;.cancel()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Debounce (trailing only)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;debounce(fn, 300)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs once, 300ms after calls stop. Best for search/autosave.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throttle (leading + trailing)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;throttle(fn, 100, { leading: true, trailing: true })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs immediately, then at most every 100ms, plus once more after the burst ends.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancel a pending call&lt;/td&gt;
&lt;td&gt;&lt;code&gt;debounced.cancel()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clears the timer; the delayed call never runs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flush a pending call now&lt;/td&gt;
&lt;td&gt;&lt;code&gt;debounced.flush()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs the pending call immediately instead of waiting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual/animation rate-limit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;requestAnimationFrame&lt;/code&gt; loop&lt;/td&gt;
&lt;td&gt;Caps work to once per paint; smoother than a fixed-ms throttle for scroll/resize.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React: create once&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useMemo(() =&amp;gt; debounce(fn, ms), [])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Never recreate inside the render body.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React: cleanup&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useEffect(() =&amp;gt; () =&amp;gt; debounced.cancel(), [])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevents calls firing after unmount.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the whole pattern, copy-paste ready&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;debounced&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;leading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trailing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;throttled&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastRun&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;lastArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leading&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trailing&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;lastRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastArgs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;throttled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;throttled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/weekly-debounce-and-throttle/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Debounce waits for quiet and runs once at the end; throttle runs on a fixed schedule no matter how long the activity continues.&lt;/li&gt;
&lt;li&gt;Every event still reaches the wrapper — what changes is how often the &lt;em&gt;real&lt;/em&gt; work behind it is allowed to run.&lt;/li&gt;
&lt;li&gt;Use debounce for "what's the final value" (search, autosave); use throttle (or &lt;code&gt;requestAnimationFrame&lt;/code&gt;) for "keep me updated while this continues" (scroll, resize, drag).&lt;/li&gt;
&lt;li&gt;Both need &lt;code&gt;cancel()&lt;/code&gt; wired into cleanup — an uncancelled debounce or throttle is a call waiting to fire on a component that no longer exists.&lt;/li&gt;
&lt;li&gt;Create the wrapped function once, not on every render.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/weekly-debounce-and-throttle/quiz" rel="noopener noreferrer"&gt;Take the 8-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That search box from the opening paragraph needs exactly one line changed — wrap the handler in &lt;code&gt;debounce(fetchResults, 300)&lt;/code&gt; — and the ten wasted requests become one, fired the moment the user actually stops typing. Which of your own event handlers is still firing ten times more than it needs to?&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>performance</category>
    </item>
    <item>
      <title>You Don't Need a WebSocket for That Live Feed</title>
      <dc:creator>Parsa Jiravand</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:11:42 +0000</pubDate>
      <link>https://dev.to/parsajiravand/you-dont-need-a-websocket-for-that-live-feed-7oa</link>
      <guid>https://dev.to/parsajiravand/you-dont-need-a-websocket-for-that-live-feed-7oa</guid>
      <description>&lt;p&gt;You need a live notification badge. The count in the corner should tick up the moment something happens on the server — no refresh, no polling every three seconds hoping you didn't miss anything. So you do what everyone does: &lt;code&gt;npm install socket.io&lt;/code&gt;, wire up a client, and ship it.&lt;/p&gt;

&lt;p&gt;Then someone's wifi hiccups for two seconds. The socket drops. Nothing reconnects, because you didn't write that part yet. Now you're writing it — a &lt;code&gt;reconnecting&lt;/code&gt; flag, a backoff timer that doubles each attempt, a cap so it doesn't retry forever, a check for whether the tab is even in the foreground. Forty-something lines later you have a hand-rolled connection manager sitting in front of a feature that only ever sent data in one direction: server to browser.&lt;/p&gt;

&lt;p&gt;There's a browser API that already does the part you just spent an afternoon on. It's been sitting in every evergreen browser for over a decade, and it's not the one you reached for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong way, in more detail than you wanted
&lt;/h2&gt;

&lt;p&gt;Here's roughly what that WebSocket reconnect logic looks like once it's "done":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss://api.example.com/notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;updateBadge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retryDelay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It's also code you didn't want to own: a manual backoff schedule, a &lt;code&gt;retryDelay&lt;/code&gt; you have to remember to reset on success, and a &lt;code&gt;close&lt;/code&gt;/&lt;code&gt;error&lt;/code&gt; interaction that's easy to get subtly wrong (fire both handlers and you'll double-schedule a reconnect). And you still haven't handled &lt;em&gt;resuming&lt;/em&gt; — if the connection drops mid-stream, you have no idea what the server sent while you were gone. You just start fresh and hope you didn't miss the one notification that mattered.&lt;/p&gt;

&lt;p&gt;None of that is a WebSocket problem, exactly. It's what you get for using a bidirectional, binary-capable protocol for a feature that only ever pushes text from server to client.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're actually asking for
&lt;/h2&gt;

&lt;p&gt;Strip the feature down to what it needs: the server pushes updates, the browser listens, and if the connection drops, it should pick back up without you writing a state machine. That's &lt;a href="https://html.spec.whatwg.org/multipage/server-sent-events.html" rel="noopener noreferrer"&gt;Server-Sent Events&lt;/a&gt; — an HTTP response that never ends, plus a browser-side API called &lt;code&gt;EventSource&lt;/code&gt; that reads it.&lt;/p&gt;

&lt;p&gt;The client side is almost embarrassingly small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;updateBadge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// the browser is already retrying — this just tells you it happened&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connection dropped, reconnecting…&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No &lt;code&gt;retryDelay&lt;/code&gt;, no &lt;code&gt;setTimeout&lt;/code&gt;, no &lt;code&gt;connect()&lt;/code&gt; function to call again. If the connection drops — the wifi hiccup, a proxy timeout, the server restarting — the browser reconnects on its own. You didn't write that part because it isn't yours to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that replaces your backoff timer
&lt;/h2&gt;

&lt;p&gt;The server side is a plain HTTP response with one specific content type and a text format the browser knows how to parse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each message is a &lt;code&gt;data:&lt;/code&gt; line followed by a blank line — that blank line is what tells the browser "this event is complete, hand it to &lt;code&gt;onmessage&lt;/code&gt;." Send that content type and keep the connection open, and you've built the entire server half of what &lt;code&gt;socket.io&lt;/code&gt; was doing for you, minus the parts you didn't need.&lt;/p&gt;

&lt;p&gt;The reconnection is where it earns the comparison to your backoff timer. When the browser reconnects after a drop, it's not just retrying blindly — it sends a &lt;code&gt;Last-Event-ID&lt;/code&gt; header carrying the ID of the last event it successfully received, so the server can pick the stream back up instead of replaying everything or losing the gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;send&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`id: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last-event-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// your own backlog logic&lt;/span&gt;
  &lt;span class="nx"&gt;missed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You still have to write &lt;code&gt;notifications.since()&lt;/code&gt; — SSE gives you the &lt;em&gt;mechanism&lt;/em&gt; for catching up, not the backlog itself. But that's a data-layer problem you'd have had with a WebSocket too, except there you'd also be reimplementing the ID and the reconnect trigger by hand.&lt;/p&gt;

&lt;p&gt;Want to change how long the browser waits before its first reconnect attempt? The server can set that too, with a &lt;code&gt;retry:&lt;/code&gt; line — the value's in milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip it and the browser falls back to its own default of a few seconds, which is fine for most dashboards and badges.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎮 Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bestpractic.org/blog/server-sent-events-eventsource-live-updates/playground" rel="noopener noreferrer"&gt;▶️ Open the interactive playground →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Runs right in your browser — poke at it and watch the concept react live.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops being the right tool
&lt;/h2&gt;

&lt;p&gt;SSE isn't a WebSocket replacement — it's the right tool for a narrower job, and it's worth being honest about the edges before you reach for it everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's one-way.&lt;/strong&gt; The stream only carries server → client. If the browser needs to send something back — a chat message, a cursor position, a game input — that's a normal &lt;code&gt;fetch&lt;/code&gt; call on the side, not a message over the same connection. The moment the client needs to &lt;em&gt;talk back&lt;/em&gt; in real time, you're back to WebSockets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's text only.&lt;/strong&gt; Every event is UTF-8. Binary data — audio chunks, protobuf, anything you'd otherwise send as a &lt;code&gt;Blob&lt;/code&gt; or &lt;code&gt;ArrayBuffer&lt;/code&gt; — has to be base64-encoded first, which is exactly the kind of tax that makes WebSockets or WebRTC the better call for that payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP/1.1 caps you at six connections per browser, per origin.&lt;/strong&gt; Because each &lt;code&gt;EventSource&lt;/code&gt; holds a request open indefinitely, a user with several tabs open to your app can exhaust that limit and starve other requests on the same domain. Serve your app over HTTP/2 (most CDNs and reverse proxies do this by default now) and the cap disappears, because HTTP/2 multiplexes many streams over one connection — but it's worth checking, not assuming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your feature needs the client to send data in the same real-time loop, or needs binary frames, that's your WebSocket. If it's a one-way trickle of small text updates — a badge, a progress indicator, a live dashboard number, a "someone else is editing this doc" banner — you were reaching for the heavier tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Test yourself
&lt;/h2&gt;

&lt;p&gt;Think it clicked? &lt;strong&gt;&lt;a href="https://bestpractic.org/blog/server-sent-events-eventsource-live-updates/quiz" rel="noopener noreferrer"&gt;Take the 8-question quiz →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next time a feature description is "push updates from the server, nothing coming back," open your network tab before you open your &lt;code&gt;package.json&lt;/code&gt;. Check whether &lt;code&gt;EventSource&lt;/code&gt; covers it first — you might delete a reconnect handler instead of writing one.&lt;/p&gt;

&lt;p&gt;Have you shipped a WebSocket for something that turned out to be one-way the whole time? Tell me what it was — I'd bet it's more common than the socket.io download numbers suggest.&lt;/p&gt;




&lt;p&gt;🚀 &lt;strong&gt;Want more like this?&lt;/strong&gt; Every guide, playground, and quiz lives on &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;bestpractic.org&lt;/a&gt;&lt;/strong&gt; — open it and &lt;strong&gt;&lt;a href="https://bestpractic.org/" rel="noopener noreferrer"&gt;sign up free&lt;/a&gt;&lt;/strong&gt; so the next one finds you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Let's stay connected:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub&lt;/strong&gt; — follow me and star the projects: &lt;a href="https://github.com/parsajiravand" rel="noopener noreferrer"&gt;github.com/parsajiravand&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;Discord&lt;/strong&gt; — join the frontend best-practices community: &lt;a href="https://discord.gg/d9KRhuAwQ" rel="noopener noreferrer"&gt;discord.gg/d9KRhuAwQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📸 &lt;strong&gt;Instagram&lt;/strong&gt; — frontend best practices, daily: &lt;a href="https://www.instagram.com/bestpractice___/" rel="noopener noreferrer"&gt;@bestpractice___&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
  </channel>
</rss>
