<?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: Anas Sheikh</title>
    <description>The latest articles on DEV Community by Anas Sheikh (@anas_sheikh_2).</description>
    <link>https://dev.to/anas_sheikh_2</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%2F4023868%2Fa72627f1-0556-4d31-aa00-0cc6ada7c4fb.png</url>
      <title>DEV Community: Anas Sheikh</title>
      <link>https://dev.to/anas_sheikh_2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anas_sheikh_2"/>
    <language>en</language>
    <item>
      <title>Your Server Component Might Be Fetching Data Three Times Slower Than It Needs To</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Wed, 30 Sep 2026 09:34:25 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/your-server-component-might-be-fetching-data-three-times-slower-than-it-needs-to-1bkb</link>
      <guid>https://dev.to/anas_sheikh_2/your-server-component-might-be-fetching-data-three-times-slower-than-it-needs-to-1bkb</guid>
      <description>&lt;p&gt;This is one of the easiest performance mistakes to write without noticing, since the code reads completely naturally, top to bottom, each line waiting for the one before it to finish, exactly the way most people learn to write async code in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup That Reads Perfectly Naturally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/dashboard/page.tsx&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;DashboardPage&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;user&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;getUser&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;getPosts&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;comments&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;getComments&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;analytics&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;getAnalytics&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="nc"&gt;Dashboard&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;analytics&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;Four &lt;code&gt;await&lt;/code&gt; calls, one after another. Clean, readable, and exactly how sequential code naturally gets written when each line is just "get the next thing I need." The problem is that none of these four calls actually depend on the results of the others, &lt;code&gt;getPosts&lt;/code&gt; doesn't need anything from &lt;code&gt;getUser&lt;/code&gt;, &lt;code&gt;getComments&lt;/code&gt; doesn't need anything from &lt;code&gt;getPosts&lt;/code&gt;, and so on. They're all independently fetchable at the same time, and writing them sequentially means the total page load time is the sum of all four, not the time of the single slowest one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Costs More Than It Looks Like It Should
&lt;/h2&gt;

&lt;p&gt;If each of these four calls takes roughly 150 milliseconds, a genuinely reasonable time for a real database query, the sequential version takes roughly 600 milliseconds total, each one waiting for the previous one to fully complete before even starting. Run the same four calls concurrently instead, and the total time is roughly 150 milliseconds, the time of the single slowest call, since they're all genuinely happening at once rather than queued up one behind another for no actual reason.&lt;/p&gt;

&lt;p&gt;This is a real, measurable difference on a page real users are waiting for, and it's entirely avoidable, since nothing about these four pieces of data actually requires this sequential order, the code just happens to be written in a shape that imposes one anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Fix: Promise.all for Independent Fetches
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&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;DashboardPage&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;user&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="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;analytics&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;getComments&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;getAnalytics&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="nc"&gt;Dashboard&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;analytics&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;&lt;code&gt;Promise.all&lt;/code&gt; starts all four requests essentially simultaneously and waits for all of them to complete together, rather than starting each one only after the last one finished. Same data, same final render, a fraction of the total wait, purely by removing an ordering constraint that was never actually necessary in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Gets Genuinely Trickier: Partial Dependencies
&lt;/h2&gt;

&lt;p&gt;Not every case is this clean. Sometimes one piece of data genuinely depends on another, and mixing dependent and independent fetches needs a bit more care than a single flat &lt;code&gt;Promise.all&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;// ❌ Everything sequential, even though posts and analytics don't need each other&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;DashboardPage&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;user&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;getUser&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;getPostsForUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="c1"&gt;// genuinely depends on user&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analytics&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;getAnalytics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// does NOT depend on user at all&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="nc"&gt;Dashboard&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;analytics&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;/code&gt;&lt;/pre&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;// ✅ Only the genuine dependency stays sequential, everything else runs alongside it&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;DashboardPage&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;analyticsPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAnalytics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// starts immediately, not awaited yet&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;getUser&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;getPostsForUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="c1"&gt;// genuinely needs user.id first&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analytics&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;analyticsPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// was already running this whole time&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="nc"&gt;Dashboard&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;analytics&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting &lt;code&gt;getAnalytics()&lt;/code&gt; without immediately awaiting it lets that request begin running in the background while &lt;code&gt;getUser&lt;/code&gt; and &lt;code&gt;getPostsForUser&lt;/code&gt; proceed through their genuine, necessary sequence. By the time the code actually needs the analytics result, it's very likely already finished, or at least has been running the whole time instead of only starting after everything else completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Spot This in Your Own Code
&lt;/h2&gt;

&lt;p&gt;Look for multiple &lt;code&gt;await&lt;/code&gt; statements in a row where each one calls a genuinely independent function, nothing on the left side of an earlier line appears as an argument to a later one. That absence of a real data dependency is the signal, these calls have no actual reason to be sequential, and wrapping them in &lt;code&gt;Promise.all&lt;/code&gt; is very likely a free, meaningful performance improvement with zero behavior change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-B2&lt;/span&gt; &lt;span class="nt"&gt;-A2&lt;/span&gt; &lt;span class="s2"&gt;"await get"&lt;/span&gt; app/&lt;span class="k"&gt;**&lt;/span&gt;/page.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scanning for clusters of consecutive &lt;code&gt;await&lt;/code&gt; calls to different functions, then checking whether any of them genuinely reference the previous line's result, is a quick, practical way to find real waterfall candidates across an existing codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters More on Pages With Several Independent Data Sources
&lt;/h2&gt;

&lt;p&gt;A dashboard, a page with several distinct widgets or sections each backed by their own query, is exactly where this pattern accumulates the most cost, since it's common to have four, five, or more genuinely independent pieces of data feeding one page, each one innocently written as its own &lt;code&gt;await&lt;/code&gt; line, each one adding its full latency on top of all the others instead of overlapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before awaiting multiple async calls sequentially, check whether any of them genuinely need a previous call's result. If none do, &lt;code&gt;Promise.all&lt;/code&gt; turns their combined wait time from a sum into a maximum, for free, with no behavior change.&lt;/strong&gt; For a genuine mix of dependent and independent calls, start the independent ones early without immediately awaiting them, so they run in the background while the real, necessary sequence proceeds.&lt;/p&gt;

&lt;p&gt;I check every Server Component I write for exactly this pattern now, across client work and the dashboards and templates I build at &lt;a href="https://www.pixelanas.com" rel="noopener noreferrer"&gt;pixelanas.com&lt;/a&gt;, since it's one of the highest-value, lowest-effort performance fixes available, and I've written about related data-fetching patterns on the &lt;a href="https://www.pixelanas.com/blog" rel="noopener noreferrer"&gt;blog&lt;/a&gt; too.&lt;/p&gt;




&lt;p&gt;Go check your own dashboard or any data-heavy page for consecutive &lt;code&gt;await&lt;/code&gt; calls with no genuine dependency between them. If you find some, wrapping them in &lt;code&gt;Promise.all&lt;/code&gt; is very likely a free win sitting right there. Drop what you find in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>performance</category>
      <category>typescript</category>
      <category>react</category>
    </item>
    <item>
      <title>Calling revalidateTag on an Already-Uncached Fetch Does Nothing, and Next.js Won't Tell You</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Tue, 29 Sep 2026 09:06:50 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/calling-revalidatetag-on-an-already-uncached-fetch-does-nothing-and-nextjs-wont-tell-you-12k2</link>
      <guid>https://dev.to/anas_sheikh_2/calling-revalidatetag-on-an-already-uncached-fetch-does-nothing-and-nextjs-wont-tell-you-12k2</guid>
      <description>&lt;p&gt;This is a genuinely quiet failure mode, since calling &lt;code&gt;revalidateTag&lt;/code&gt; never errors, never warns, and appears to complete successfully every single time, whether or not it actually invalidated anything at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup That Looks Correct
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/queries/posts.ts&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;getPosts&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tags&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;posts&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="na"&gt;cache&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-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// added later, maybe for a different, unrelated reason&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// actions/posts.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;revalidateTag&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;createPost&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;revalidateTag&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;// runs without error, but does genuinely nothing here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tag is there. The revalidation call is there. Nothing throws. And this specific combination doesn't actually do what it looks like it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Specific Combination Is a No-Op
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cache: 'no-store'&lt;/code&gt; tells Next.js to never cache this fetch's result at all, fetch it fresh, every single time, full stop. Tagging with &lt;code&gt;next: { tags: [...] }&lt;/code&gt; only matters for a fetch whose result is actually being cached, since the tag is metadata attached to a cache entry, used to find and invalidate that entry later. If there's no cache entry in the first place, because &lt;code&gt;no-store&lt;/code&gt; explicitly prevented one from ever being created, the tag has nothing to attach to, and &lt;code&gt;revalidateTag('posts')&lt;/code&gt; has nothing to actually invalidate when it runs. It completes, technically successfully, having done nothing, since there was never anything cached for it to clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Easy to End Up With Without Noticing
&lt;/h2&gt;

&lt;p&gt;This combination rarely gets written deliberately in one sitting, it usually accumulates. A fetch starts out cached, tagged, and working correctly with &lt;code&gt;revalidateTag&lt;/code&gt;. Later, someone adds &lt;code&gt;cache: 'no-store'&lt;/code&gt; to that same fetch, for a completely unrelated reason, debugging a different staleness issue, following advice for a different part of the app, copying a pattern from elsewhere, without registering that it silently makes the existing tag, and everything relying on &lt;code&gt;revalidateTag('posts')&lt;/code&gt; elsewhere in the codebase, functionally inert.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Silent Gap at the Route Level
&lt;/h2&gt;

&lt;p&gt;This isn't limited to the fetch call itself. A route-level override has the identical effect on every fetch within that route, regardless of what caching options those individual fetch calls specify:&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/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;force-dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// forces every fetch on this route to be uncached&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;BlogPage&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;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;getPosts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// tags and cache options here are now irrelevant&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="nc"&gt;PostList&lt;/span&gt; &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;export const dynamic = 'force-dynamic'&lt;/code&gt; overrides caching behavior for the entire route, meaning any &lt;code&gt;tags&lt;/code&gt; option on any fetch within it becomes similarly inert, even if that specific fetch call looks completely correctly configured for tag-based revalidation on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Nothing Warns You About This
&lt;/h2&gt;

&lt;p&gt;Both &lt;code&gt;cache: 'no-store'&lt;/code&gt; and &lt;code&gt;next: { tags: [...] }&lt;/code&gt; are individually valid, correct options, and Next.js has no way to know that combining them on the same fetch reflects a genuine mismatch in intent rather than a deliberate choice, since there's nothing inherently wrong with either option on its own. The framework can't distinguish "I meant to cache this and tag it, but also told it never to cache" from "I have some other genuine reason for both of these together," so it simply does exactly what each option says, individually, with no cross-check between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Catch This
&lt;/h2&gt;

&lt;p&gt;Check whether &lt;code&gt;revalidateTag&lt;/code&gt; calls are actually having an effect, not just whether they run without error. Trigger the mutation, then check whether the next read genuinely reflects the change:&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;// A quick manual check: create a post, then immediately check whether&lt;/span&gt;
&lt;span class="c1"&gt;// the posts list reflects it without a hard refresh or waiting on&lt;/span&gt;
&lt;span class="c1"&gt;// the fetch's own natural cache lifetime to expire&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the change doesn't show up until a hard refresh or the cache's own natural expiration, despite &lt;code&gt;revalidateTag&lt;/code&gt; having run, that's this exact mismatch, worth checking the actual fetch options and any route-level &lt;code&gt;dynamic&lt;/code&gt; export for a conflict between "never cache this" and "this is tagged for cache invalidation."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A &lt;code&gt;revalidateTag&lt;/code&gt; call only does real work against a fetch that is actually being cached under that tag, not merely tagged.&lt;/strong&gt; &lt;code&gt;cache: 'no-store'&lt;/code&gt; on the same fetch, or &lt;code&gt;dynamic = 'force-dynamic'&lt;/code&gt; at the route level, both silently remove the cache entry that tag was supposed to point at, and the revalidation call becomes a harmless, successful-looking no-op. When adding or changing caching options on an existing fetch, it's worth explicitly checking whether that fetch is relied on elsewhere for tag-based revalidation, since the two settings can drift out of sync without either one looking wrong in isolation.&lt;/p&gt;

&lt;p&gt;I run into this specific mismatch most often on projects where caching gets tuned incrementally over time, exactly the kind of pattern I try to catch early in the SaaS dashboards and templates I build at &lt;a href="https://www.pixelanas.com" rel="noopener noreferrer"&gt;pixelanas.com&lt;/a&gt;. If this kind of caching nuance is useful, I go deeper into related patterns on the &lt;a href="https://www.pixelanas.com/blog" rel="noopener noreferrer"&gt;blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you've got a &lt;code&gt;revalidateTag&lt;/code&gt; call somewhere in your codebase, worth actually verifying it's doing real work right now, not just that it runs without error. Check the fetch it's supposed to be targeting for a &lt;code&gt;cache: 'no-store'&lt;/code&gt; or route-level &lt;code&gt;force-dynamic&lt;/code&gt; quietly canceling it out. Drop what you find in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>redis</category>
      <category>typescript</category>
    </item>
    <item>
      <title>You Added a Content-Security-Policy Header and Your Next.js App Went Blank. Here's Why.</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:43:12 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/you-added-a-content-security-policy-header-and-your-nextjs-app-went-blank-heres-why-5bb5</link>
      <guid>https://dev.to/anas_sheikh_2/you-added-a-content-security-policy-header-and-your-nextjs-app-went-blank-heres-why-5bb5</guid>
      <description>&lt;p&gt;You read that a Content-Security-Policy header is a good security hardening step. You add a sensible-looking policy to your Next.js config, deploy, and the site loads as a blank or half-working page with a wall of red errors in the console.&lt;/p&gt;

&lt;p&gt;Nothing is wrong with your code. The policy is doing exactly what you told it to do, and what you told it to do happens to block Next.js itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Header That Breaks Everything
&lt;/h2&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="kd"&gt;const&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;headers&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;{&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/(.*)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&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="na"&gt;key&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-Security-Policy&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;default-src 'self'; script-src 'self'&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="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="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;code&gt;script-src 'self'&lt;/code&gt; says scripts may only load from your own origin as external files. That sounds right, until you remember Next.js injects small inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags into every page, for hydration data, for the router, for streaming. Inline scripts are exactly what &lt;code&gt;script-src 'self'&lt;/code&gt; forbids, so the browser refuses to run them, hydration never completes, and the page looks dead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tempting Wrong Fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&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;default-src 'self'; script-src 'self' 'unsafe-inline'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the errors disappear, and it also removes most of the protection you added the header for. &lt;code&gt;'unsafe-inline'&lt;/code&gt; tells the browser to run any inline script, including one an attacker managed to inject through an XSS hole. A CSP with &lt;code&gt;'unsafe-inline'&lt;/code&gt; in &lt;code&gt;script-src&lt;/code&gt; still exists, but it no longer does the main job a CSP is supposed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Fix: A Per-Request Nonce
&lt;/h2&gt;

&lt;p&gt;A nonce is a random, single-use value generated for each request. You put it in the CSP header, and you attach the same value to the scripts you trust. The browser then only runs inline scripts carrying the matching nonce, and an injected script has no way to guess it.&lt;/p&gt;

&lt;p&gt;Next.js supports this through middleware:&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;// middleware.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;NextResponse&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/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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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/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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&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="nx"&gt;NextRequest&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;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&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;csp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default-src 'self'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`script-src 'self' 'nonce-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' 'strict-dynamic'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`style-src 'self' 'nonce-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nonce&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;img-src 'self' blob: data:&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="s2"&gt;object-src 'none'&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="s2"&gt;base-uri 'self'&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;requestHeaders&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;Headers&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="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;requestHeaders&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;x-nonce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;requestHeaders&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-Security-Policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;csp&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestHeaders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;response&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="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-Security-Policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;csp&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;response&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;Next.js reads the nonce from the request's CSP header and applies it to the framework's own inline scripts automatically. For any script you add yourself, read the nonce and pass it along:&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&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;headers&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="nx"&gt;Script&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/script&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;nonce&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;headers&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;x-nonce&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="kc"&gt;undefined&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="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;Script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://analytics.example.com/script.js"&lt;/span&gt; &lt;span class="na"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"afterInteractive"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;'strict-dynamic'&lt;/code&gt; in the policy lets a nonced script load further scripts it depends on, which keeps things like analytics loaders working without you allowlisting every domain by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tradeoff Nobody Mentions Upfront
&lt;/h2&gt;

&lt;p&gt;A nonce has to be unique per request, and it has to be baked into the HTML the server sends. That means any page using it must be rendered dynamically on each request. A statically generated page is built once ahead of time, so it cannot contain a fresh nonce for every visitor.&lt;/p&gt;

&lt;p&gt;This is the same static versus dynamic rendering boundary covered in my earlier posts on &lt;code&gt;cookies()&lt;/code&gt; and &lt;code&gt;searchParams&lt;/code&gt;. Reading the nonce from &lt;code&gt;headers()&lt;/code&gt; opts the route into dynamic rendering, so turning on nonce-based CSP across your whole site gives up static generation for every route it covers.&lt;/p&gt;

&lt;p&gt;For a mostly static marketing site, that is a real cost. Two reasonable ways to handle it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apply the strict nonce policy only where it matters most&lt;/strong&gt;, like authenticated dashboards and anything handling user input, and use a hash-based or more permissive policy for purely static pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use hash-based CSP for static pages&lt;/strong&gt;, where the script contents are known at build time, so each inline script's hash can be allowlisted instead of requiring a per-request nonce.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Test It Without Breaking Production
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ship this first, it reports violations without blocking anything&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;key&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-Security-Policy-Report-Only&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="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default-src 'self'; script-src 'self' 'nonce-...'&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;&lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; logs every violation to the console and to a reporting endpoint without actually blocking a thing. Run it for a while, fix what it flags, then switch to the enforcing header. This is how you avoid the blank-page deploy from the top of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A strict CSP and Next.js's inline scripts are incompatible unless you use nonces or hashes, and nonces mean dynamic rendering.&lt;/strong&gt; Do not reach for &lt;code&gt;'unsafe-inline'&lt;/code&gt; to make errors go away, since that removes the protection you were adding. Start in report-only mode, decide which routes genuinely need the strictest policy, and accept that those routes will render per request.&lt;/p&gt;

&lt;p&gt;I go through this same tradeoff when hardening client projects and the templates at &lt;a href="https://www.pixelanas.com" rel="noopener noreferrer"&gt;pixelanas.com&lt;/a&gt;, and I write up more of these security and rendering details on the &lt;a href="https://www.pixelanas.com/blog" rel="noopener noreferrer"&gt;blog&lt;/a&gt; as I run into them.&lt;/p&gt;




&lt;p&gt;If you have a CSP on a Next.js app right now, check whether it contains &lt;code&gt;'unsafe-inline'&lt;/code&gt; in &lt;code&gt;script-src&lt;/code&gt;. If it does, it is worth knowing how much protection it is really giving you. Drop what you find in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>security</category>
      <category>java</category>
    </item>
    <item>
      <title>That Third-Party Script Tag You Copy-Pasted Is Probably Hurting</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Sun, 27 Sep 2026 09:35:19 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/that-third-party-script-tag-you-copy-pasted-is-probably-hurting-4pfi</link>
      <guid>https://dev.to/anas_sheikh_2/that-third-party-script-tag-you-copy-pasted-is-probably-hurting-4pfi</guid>
      <description>&lt;p&gt;Chat widgets, analytics snippets, ad network tags, review platform embeds, nearly all of them ship with a copy-paste &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; snippet meant for a plain HTML page, and pasted directly into a Next.js app without modification, several of them quietly do real damage to your actual performance metrics for content that has nothing to do with the widget itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Copy-Paste That Looks Harmless
&lt;/h2&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&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="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="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;head&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;script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://widget.example.com/embed.js"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&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;head&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="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;This is exactly the snippet most third-party services hand you, drop this in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, done. It works, the widget shows up. It also, by default, blocks the browser from continuing to parse and render the rest of the page until this script finishes downloading and executing, regardless of whether the widget is something a visitor actually needs immediately, a chat bubble in the corner, or something they'll never notice loading a beat later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Specifically Matters for Your Core Web Vitals
&lt;/h2&gt;

&lt;p&gt;A plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag with no special handling is render-blocking by default. For a chat widget, a review platform embed, most analytics tools, none of which are part of the actual content a visitor came to see, this render-blocking behavior directly delays your Largest Contentful Paint and can hurt Time to Interactive, real, measured metrics that affect both user experience and search ranking, all for a script whose own functionality genuinely doesn't need to block anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What next/script Actually Solves
&lt;/h2&gt;

&lt;p&gt;Next.js provides &lt;code&gt;next/script&lt;/code&gt; specifically to give you real control over when and how a third-party script loads, rather than accepting the plain HTML default of "block everything until this finishes."&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Script&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/script&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="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="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="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;Script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://widget.example.com/embed.js"&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazyOnload"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;strategy&lt;/code&gt; prop is where the actual decision happens, and choosing the right one for a given script matters far more than just using &lt;code&gt;next/script&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strategies, and What They're Actually For
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;beforeInteractive&lt;/code&gt;&lt;/strong&gt; loads and executes before the page becomes interactive, before hydration. This is reserved for genuinely critical scripts the page cannot function correctly without, a polyfill required for the page to render at all, certain bot detection required before any interaction. Using this for a chat widget or analytics script defeats the entire purpose, it's just as render-blocking as the plain, unmodified &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, applied to something that never needed that priority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;afterInteractive&lt;/code&gt;&lt;/strong&gt; (the default when &lt;code&gt;strategy&lt;/code&gt; isn't specified) loads after the page becomes interactive, a reasonable default for most analytics and tracking scripts that need to start running reasonably early but don't need to block initial render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;lazyOnload&lt;/code&gt;&lt;/strong&gt; loads during idle browser time, well after everything else has finished, appropriate for genuinely low-priority scripts, a chat widget, a social media embed, anything a visitor doesn't need in the first few seconds and won't notice arriving slightly later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where People Get This Wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Using the plain, unmodified &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag from a provider's documentation, skipping &lt;code&gt;next/script&lt;/code&gt; entirely&lt;/strong&gt;, which defaults to fully render-blocking behavior in every case, since a plain HTML script tag has no strategy concept at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;next/script&lt;/code&gt; but leaving every third-party embed on the default &lt;code&gt;afterInteractive&lt;/code&gt; strategy, regardless of actual priority.&lt;/strong&gt; A genuinely low-priority widget, a review platform badge, a social share button set, sitting on &lt;code&gt;afterInteractive&lt;/code&gt; still competes for the browser's attention earlier than it needs to, when &lt;code&gt;lazyOnload&lt;/code&gt; would serve it identically from the visitor's perspective while doing measurably less damage to earlier, more important metrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting &lt;code&gt;beforeInteractive&lt;/code&gt; on something that isn't actually critical, sometimes copied directly from an example without understanding what that specific strategy is reserved for.&lt;/strong&gt; This is the most damaging mistake, since it explicitly requests the most aggressive, most blocking loading behavior available, for a script that almost certainly didn't need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Way to Choose the Right Strategy
&lt;/h2&gt;

&lt;p&gt;Ask honestly, does the page genuinely not function correctly without this script running before anything else. If the honest answer is no, and for the overwhelming majority of third-party embeds it is no, &lt;code&gt;beforeInteractive&lt;/code&gt; is off the table. Between &lt;code&gt;afterInteractive&lt;/code&gt; and &lt;code&gt;lazyOnload&lt;/code&gt;, the real question is whether a visitor would notice or care if this specific widget took an extra second or two to actually appear. If not, &lt;code&gt;lazyOnload&lt;/code&gt; is almost always the better choice, genuinely no different from the visitor's perspective, meaningfully better for your actual performance metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Your Own Third-Party Scripts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;script"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.tsx"&lt;/span&gt; app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any raw &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag this turns up, not using &lt;code&gt;next/script&lt;/code&gt; at all, is a candidate worth converting. For every existing &lt;code&gt;next/script&lt;/code&gt; usage, check whether the strategy actually matches the script's real priority, rather than whatever the default happened to be when it was added.&lt;/p&gt;

&lt;p&gt;I audit exactly this on client projects and keep it deliberate across the templates I build at &lt;a href="https://www.pixelanas.com" rel="noopener noreferrer"&gt;pixelanas.com&lt;/a&gt;, since a slow Lighthouse score caused entirely by a chat widget nobody consciously chose to prioritize is one of the more avoidable, easy-to-fix performance issues out there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every third-party script needs a deliberately chosen loading strategy, not whatever a provider's copy-paste snippet or a framework default happens to apply.&lt;/strong&gt; &lt;code&gt;beforeInteractive&lt;/code&gt; for genuinely critical scripts only, &lt;code&gt;afterInteractive&lt;/code&gt; for scripts that need to run reasonably early, &lt;code&gt;lazyOnload&lt;/code&gt; for everything else, which, in practice, is most third-party embeds most sites actually use.&lt;/p&gt;




&lt;p&gt;If you've got third-party scripts loaded as plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, or sitting on a strategy nobody actually chose deliberately, worth auditing them against a real Lighthouse report before and after. Drop what you find in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>performance</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Forgetting default.tsx in a Next.js Parallel Route Can 404 Your Entire Page, Not Just One Slot</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Sat, 26 Sep 2026 08:44:48 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/forgetting-defaulttsx-in-a-nextjs-parallel-route-can-404-your-entire-page-not-just-one-slot-2b3j</link>
      <guid>https://dev.to/anas_sheikh_2/forgetting-defaulttsx-in-a-nextjs-parallel-route-can-404-your-entire-page-not-just-one-slot-2b3j</guid>
      <description>&lt;p&gt;This is a genuinely surprising failure mode the first time you hit it, since the natural assumption is that an unmatched parallel route slot would just render nothing, not take down a page that has nothing to do with the slot in question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup From an Earlier Post, Extended
&lt;/h2&gt;

&lt;p&gt;Building on the intercepting route modal pattern from an earlier post, the &lt;code&gt;@modal&lt;/code&gt; slot renders the modal when a specific route is intercepted. But a parallel route slot needs to render &lt;em&gt;something&lt;/em&gt; for every URL within its scope, not just the one specific route it was built for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── @modal/
│   ├── (.)photo/
│   │   └── [id]/
│   │       └── page.tsx    // renders when a photo route is intercepted
│   └── (missing: default.tsx)
├── photo/
│   └── [id]/
│       └── page.tsx
├── settings/
│   └── page.tsx             // a completely unrelated route
├── layout.tsx
└── page.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Visiting an Unrelated Page Can Break Entirely
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;@modal&lt;/code&gt; slot is defined at the root layout level here, which means it's part of every route rendered under that layout, not just the photo routes it was actually built for. Next.js needs to know what &lt;code&gt;@modal&lt;/code&gt; should render for every single route within its scope, including &lt;code&gt;/settings&lt;/code&gt;, which has nothing to do with photos or modals at all. If there's no &lt;code&gt;page.tsx&lt;/code&gt; inside &lt;code&gt;@modal&lt;/code&gt; matching &lt;code&gt;/settings&lt;/code&gt;, and critically, no &lt;code&gt;default.tsx&lt;/code&gt; file to serve as a fallback for exactly this situation, Next.js has no content to render for that slot on that route, and without a fallback, it treats this as a genuine 404 for the entire page, not just an empty modal slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Specific Failure Mode Is So Confusing
&lt;/h2&gt;

&lt;p&gt;The intercepting modal pattern itself works perfectly, clicking a photo from the grid correctly shows the modal, refreshing correctly shows the full page, exactly the behavior covered in the earlier post. The &lt;code&gt;/settings&lt;/code&gt; page, something that looks completely unrelated to any of that photo and modal logic, is what actually breaks, with a full 404, and nothing about that page's own code gives any indication why. The actual cause lives entirely in the parallel route configuration at the layout level, several files away from the page that's actually failing, which makes this a genuinely disorienting bug to trace the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A default.tsx for Every Parallel Slot
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/@modal/default.tsx&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;Default&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// this slot simply renders nothing when there's no active modal&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;default.tsx&lt;/code&gt; tells Next.js exactly what to render for this slot whenever the current route doesn't match anything more specific within it. Returning &lt;code&gt;null&lt;/code&gt; here is completely correct and intentional, for any route that isn't specifically showing a modal, &lt;code&gt;@modal&lt;/code&gt; should render nothing, and &lt;code&gt;default.tsx&lt;/code&gt; is what makes that an explicit, defined behavior instead of an unhandled gap that produces a full 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Easy to Miss When First Setting Up Parallel Routes
&lt;/h2&gt;

&lt;p&gt;Most tutorials and examples for intercepting routes focus entirely on the specific route being intercepted, since that's the actual feature being demonstrated, and &lt;code&gt;default.tsx&lt;/code&gt; is easy to treat as an afterthought or skip in a quick example, since the demo itself often only ever navigates between the two or three routes that were actually set up correctly. The gap only becomes visible once someone navigates to a genuinely unrelated route that was never part of the original example, exactly the kind of route a real, full application has plenty of.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every parallel route slot needs a &lt;code&gt;default.tsx&lt;/code&gt;, without exception, the moment that slot exists anywhere in a layout that also renders routes the slot has no specific content for.&lt;/strong&gt; This isn't an edge case fallback for rare situations, it's the default, expected behavior for the overwhelming majority of routes in any real application, since a slot built for one specific interaction, a modal, a sidebar variant, is inherently the exception, not the rule, across a site's full set of routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Your Own Parallel Routes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find app &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"@*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every parallel route slot this turns up, confirm a &lt;code&gt;default.tsx&lt;/code&gt; exists directly inside it. If it doesn't, any route within that layout's scope that isn't specifically handled by that slot is at genuine risk of a full 404, not a minor rendering gap, the moment someone actually navigates there.&lt;/p&gt;

&lt;p&gt;I set up a &lt;code&gt;default.tsx&lt;/code&gt; as the very first file whenever I add a parallel route slot to any project now, client work included, for exactly this reason, it costs one trivial file and prevents a genuinely confusing, hard-to-trace production bug. If you're exploring more Next.js patterns like this, I write about them regularly at &lt;a href="https://www.pixelanas.com/blog" rel="noopener noreferrer"&gt;pixelanas.com/blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you're using parallel routes anywhere, run the check above right now and confirm every slot has its fallback file. If one doesn't, you've got a real, ready-to-happen 404 waiting on some route you haven't tested yet. Drop what you find in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Why Your useEffect Runs Twice in Development (And Why You Shouldn't "Fix" It)</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Fri, 25 Sep 2026 19:59:30 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/why-your-useeffect-runs-twice-in-development-and-why-you-shouldnt-fix-it-12d2</link>
      <guid>https://dev.to/anas_sheikh_2/why-your-useeffect-runs-twice-in-development-and-why-you-shouldnt-fix-it-12d2</guid>
      <description>&lt;p&gt;This causes a very specific, very common reaction, a developer sees a &lt;code&gt;console.log&lt;/code&gt; inside &lt;code&gt;useEffect&lt;/code&gt; fire twice in the browser console during local development, assumes something's genuinely broken, and starts debugging a bug that doesn't actually exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually Happening
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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;useEffect&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Tracker&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="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="s1"&gt;Effect ran&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// logs twice in development, once in production&lt;/span&gt;
    &lt;span class="nf"&gt;trackPageView&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;return&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;In development, with React's Strict Mode enabled, which the Next.js App Router enables by default, React deliberately mounts a component, runs its effects, unmounts it, and immediately remounts it again, running the effects a second time. This is not a bug, and it's not accidental, it's a specific, intentional development-only behavior designed to help surface exactly the kind of bug this pattern causes in the first place, effects with real, unintended side effects or missing cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why React Does This on Purpose
&lt;/h2&gt;

&lt;p&gt;React is preparing for a future where components can be safely paused, discarded, and remounted, for features like offline support and certain rendering optimizations, and that capability genuinely requires effects to be safely re-runnable, mount, unmount, mount again, without producing broken or duplicated behavior. The double-invoke in development is a deliberate stress test, if your effect breaks when run twice in a row, mount, unmount, mount, it's revealing a genuine correctness gap, code that assumes it only ever runs once, which was never actually a safe assumption to begin with, even before Strict Mode made it visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Instinct to "Fix" This Is Usually Wrong
&lt;/h2&gt;

&lt;p&gt;Seeing double logs or double network calls in development and reaching for a way to suppress it, disabling Strict Mode entirely, or adding a fragile &lt;code&gt;useRef&lt;/code&gt; guard specifically to prevent the second invocation, treats the symptom as the problem. The actual problem, if there is one, is that the effect wasn't written to safely handle being cleaned up and re-run, which is a real gap worth fixing properly, not hiding by preventing Strict Mode from ever revealing it again.&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;// ❌ A common "fix" that just hides the signal instead of addressing what it revealed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasRun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;hasRun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="nf"&gt;trackPageView&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;This pattern specifically defeats the entire point of Strict Mode's double-invoke check, and worse, it can behave inconsistently across React versions and concurrent rendering scenarios in ways that are genuinely harder to reason about than just writing the effect correctly in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Fix: Write Effects That Are Safe to Run Twice
&lt;/h2&gt;

&lt;p&gt;For something like an analytics call, the real question worth asking is whether firing it twice in development actually matters, and for most analytics providers, it doesn't, in production, without Strict Mode's double-invoke, it only fires once anyway. The development-only double log is not the same as double-counting real production analytics data, a distinction worth confirming directly rather than assuming.&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="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="c1"&gt;// In production, this fires once. In development, Strict Mode fires it&lt;/span&gt;
  &lt;span class="c1"&gt;// twice, which is expected and does not reflect production behavior.&lt;/span&gt;
  &lt;span class="nf"&gt;trackPageView&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;For an effect that sets up something genuinely stateful, a subscription, an interval, an event listener, the actual fix is a real, correct cleanup function, which is precisely what Strict Mode's double-invoke is designed to verify you've written:&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interval&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;checkForUpdates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&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="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// correctly cleans up, safe to run twice&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;With a correct cleanup function, the double-invoke in development runs cleanly, set up, tear down, set up again, exactly the pattern Strict Mode exists to confirm your code actually handles safely. If removing the cleanup function causes something to visibly break or duplicate in development, that's Strict Mode doing its job, not React misbehaving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Actually Matters for Real Bugs, Not Just Console Noise
&lt;/h2&gt;

&lt;p&gt;This connects directly to the stale fetch race condition covered in an earlier post, an effect fetching data without a cleanup function that ignores stale responses is exactly the kind of effect that also breaks under Strict Mode's double-invoke in development, showing a flash of duplicated or incorrect state before settling, which is a genuine, useful early warning for a bug that would otherwise only surface in production under real network timing variance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A &lt;code&gt;useEffect&lt;/code&gt; that produces visibly different or broken behavior when Strict Mode runs it twice in development almost always has a real gap, missing or incorrect cleanup, not properly guarding against being re-invoked, that would eventually cause a genuine bug in production too, just under different, harder-to-reproduce conditions.&lt;/strong&gt; The fix is writing the effect to genuinely handle mount, unmount, remount correctly, not suppressing the specific mechanism designed to reveal that it doesn't.&lt;/p&gt;

&lt;p&gt;I keep Strict Mode enabled across every project I build, client work and the templates at &lt;a href="https://www.pixelanas.com" rel="noopener noreferrer"&gt;pixelanas.com&lt;/a&gt; alike, specifically because catching this category of bug in development, as annoying as the double logs are at first, is far cheaper than debugging the same underlying issue once it surfaces as an intermittent, hard-to-reproduce production bug instead.&lt;/p&gt;




&lt;p&gt;If you've disabled Strict Mode, or added a ref-based guard specifically to suppress double-invoked effects, worth reconsidering whether that's hiding a real, fixable gap rather than actually solving anything. Drop your own experience with this in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Your useEffect Data Fetch Might Show Stale Data If a User Navigates Quickly</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Thu, 24 Sep 2026 07:42:12 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/your-useeffect-data-fetch-might-show-stale-data-if-a-user-navigates-quickly-4bij</link>
      <guid>https://dev.to/anas_sheikh_2/your-useeffect-data-fetch-might-show-stale-data-if-a-user-navigates-quickly-4bij</guid>
      <description>&lt;p&gt;This is a genuinely common race condition, and it's specifically the kind that only shows up under real usage conditions, someone clicking between items quickly, a slow network, exactly the situations casual development testing on a fast local connection rarely reproduces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup That Looks Completely Standard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserDetail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userId&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&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="kc"&gt;null&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="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;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="nf"&gt;setUser&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;span class="nx"&gt;userId&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="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;user&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loading...&lt;/span&gt;&lt;span class="dl"&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;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;This is close to the most common way to fetch data based on a changing prop in a Client Component, and it works completely correctly the vast majority of the time. It also has a real race condition that only shows up under a specific, genuinely common sequence of events.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Exact Sequence That Triggers the Bug
&lt;/h2&gt;

&lt;p&gt;A user clicks on user A in a list, &lt;code&gt;userId&lt;/code&gt; becomes A's ID, the effect fires, a request for A's data starts. Before that request resolves, network latency, a slow connection, the user clicks user B instead, &lt;code&gt;userId&lt;/code&gt; becomes B's ID, the effect fires again, a second, separate request for B's data starts. Now two requests are in flight simultaneously.&lt;/p&gt;

&lt;p&gt;If B's request happens to resolve faster than A's, perfectly plausible depending on server load, caching, or just network variance, B's data renders correctly first. Then A's slower, now-stale request finally resolves, and its &lt;code&gt;.then()&lt;/code&gt; callback runs &lt;code&gt;setUser(data)&lt;/code&gt; with A's data, overwriting B's correct, current data with A's outdated, no-longer-relevant response. The component now displays user A's name and information while &lt;code&gt;userId&lt;/code&gt; itself is actually set to B, a genuine, visible mismatch between what's shown and what's actually selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Feels Random Rather Than Reliably Broken
&lt;/h2&gt;

&lt;p&gt;This only manifests when the specific timing lines up, a slower first request resolving after a faster second one. On a fast, consistent local connection during development, requests often resolve close enough to their sending order that this exact interleaving rarely happens, which is exactly why it's easy to ship without ever seeing it locally, and exactly why it tends to surface first as a confusing, hard-to-reproduce bug report from a real user on a real, more variable connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Fix: Ignore Stale Responses Explicitly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserDetail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userId&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&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="kc"&gt;null&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ignore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="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;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="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;ignore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setUser&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;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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ignore&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="c1"&gt;// marks this specific effect's request as stale on cleanup&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;userId&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="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;user&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loading...&lt;/span&gt;&lt;span class="dl"&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;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;The &lt;code&gt;ignore&lt;/code&gt; flag, scoped locally to each individual effect invocation, gets set to &lt;code&gt;true&lt;/code&gt; in the cleanup function, which React runs automatically the moment &lt;code&gt;userId&lt;/code&gt; changes again, before the next effect invocation starts. When A's slow request finally resolves, its specific closure's &lt;code&gt;ignore&lt;/code&gt; flag is already &lt;code&gt;true&lt;/code&gt;, since the effect was cleaned up the moment the user clicked B, and the stale &lt;code&gt;setUser(data)&lt;/code&gt; call for A's outdated data simply never happens. Only B's request, whose &lt;code&gt;ignore&lt;/code&gt; flag remains &lt;code&gt;false&lt;/code&gt; because its effect was never cleaned up, actually updates state.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Alternative Using AbortController
&lt;/h2&gt;

&lt;p&gt;For fetch specifically, &lt;code&gt;AbortController&lt;/code&gt; provides a more complete version of the same fix, actually canceling the in-flight request rather than just ignoring its eventual result:&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&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;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&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="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;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="nf"&gt;setUser&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="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="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;err&lt;/span&gt;&lt;span class="p"&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="s1"&gt;AbortError&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;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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// genuinely handle real errors, ignore expected aborts&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has a real advantage over the simple ignore flag, the actual network request gets canceled, not just its result discarded, which saves real bandwidth and server load for a request whose result was never going to be used anyway, particularly valuable for anything more expensive than a small JSON response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters More for Search and Filtering Specifically
&lt;/h2&gt;

&lt;p&gt;This exact pattern shows up constantly in search-as-you-type and rapid filtering interactions, exactly the kind of interface covered in an earlier post on search and filtering patterns. A user typing quickly triggers a new request on every keystroke, and without this protection, a slower response to an earlier, now-outdated keystroke can overwrite the correct results for what the user is actually searching for right now, displaying results for a query they've already moved past.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Any effect that fetches data based on a value that can change again before the fetch resolves needs a way to distinguish a stale, no-longer-relevant response from a current one.&lt;/strong&gt; A simple &lt;code&gt;ignore&lt;/code&gt; flag set in the cleanup function handles this cheaply for most cases. &lt;code&gt;AbortController&lt;/code&gt; handles it more completely, actually canceling the wasted request rather than just discarding its result, and is worth the small amount of extra code for anything with real cost behind each request, a genuinely expensive query, meaningful bandwidth, a rate-limited endpoint.&lt;/p&gt;

&lt;p&gt;I handle this exact pattern, mostly with &lt;code&gt;AbortController&lt;/code&gt; for anything backed by a real database query, across the dashboards and templates I build at &lt;a href="https://www.pixelanas.com" rel="noopener noreferrer"&gt;pixelanas.com&lt;/a&gt;, since it's exactly the kind of subtle bug that looks fine in every casual test and only shows up once real users start clicking around at real speed.&lt;/p&gt;




&lt;p&gt;If you've got a &lt;code&gt;useEffect&lt;/code&gt; fetching data based on a changing prop with no stale-response handling, go test it specifically by clicking or navigating quickly between a few items in a row. If you see a flash of the wrong data settling in, that's this exact race condition. Drop what you find in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Forgetting to Disable Draft Mode in Next.js Can Leave Unpublished Content Visible Longer Than You Think</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 11:34:45 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/forgetting-to-disable-draft-mode-in-nextjs-can-leave-unpublished-content-visible-longer-than-you-33h7</link>
      <guid>https://dev.to/anas_sheikh_2/forgetting-to-disable-draft-mode-in-nextjs-can-leave-unpublished-content-visible-longer-than-you-33h7</guid>
      <description>&lt;p&gt;I covered enabling draft mode as part of the Sanity CMS setup in an earlier post, letting an editor preview unpublished content before it goes live. There's a real, practical gap worth covering on its own, specifically what draft mode actually is and isn't scoped to, since the natural assumption about that scope is wrong in a way that causes genuine, recurring confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Draft Mode Actually Is
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/draft/route.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;draftMode&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&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="nx"&gt;Request&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;draft&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;draftMode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog/some-post&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;Calling &lt;code&gt;enable()&lt;/code&gt; sets a cookie in the current browser session, and Next.js checks for that specific cookie on subsequent requests to decide whether to fetch and render draft content instead of published content. This is genuinely useful, exactly the mechanism that lets an editor click "preview" and see an unpublished change before it goes live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Assumption That Causes Real Confusion
&lt;/h2&gt;

&lt;p&gt;The natural, intuitive assumption is that enabling draft mode is a site-wide toggle, flip it on, the whole site shows draft content to everyone, flip it off, everyone's back to published content. That's not what's actually happening. Draft mode is scoped entirely to the cookie in one specific browser, one specific person's session. It has no effect whatsoever on what any other visitor sees, and it's not a global site state at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Assumption Actually Causes Problems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;An editor previews a draft, sees it looks correct, and reports the change is live, when it isn't.&lt;/strong&gt; Since draft mode only affects their own browser, they're seeing the preview correctly, and it's genuinely easy to forget, in the moment, that what they're seeing is specifically because of their own enabled draft mode, not because the change has actually been published for everyone else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An editor forgets to disable draft mode after previewing, and later assumes the live site is broken when they see unexpected content.&lt;/strong&gt; If draft mode stays enabled in their browser from a previous preview session, every subsequent visit to the site, including regular, non-preview browsing, continues showing draft content in that specific browser, which can look exactly like the site rendering the wrong content, when it's actually correctly rendering draft content because that browser's cookie still has it enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A shared or public computer retains draft mode enabled from a previous session.&lt;/strong&gt; If draft mode was enabled on a shared device and never explicitly disabled, whoever uses that browser next, potentially someone without any editing permissions or context at all, sees draft, unpublished content without any indication of why, or that it's not what a normal visitor would see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Doesn't Feel Like a Bug When It Happens
&lt;/h2&gt;

&lt;p&gt;None of this is actually broken, draft mode is behaving exactly as designed, scoped to a cookie in one browser. The confusion comes entirely from a mismatch between that actual, correct behavior and the intuitive mental model of it as some kind of site-wide switch. Nothing in the experience of using it particularly corrects that mental model, since enabling it does show the expected preview, which reinforces the sense that "draft mode is on" as a general, site-wide state, rather than "draft mode is on, in this specific browser, right now."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Fix: Always Pair Enable With a Clear, Visible Exit Path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/disable-draft/route.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;draftMode&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;redirect&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/navigation&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;GET&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;draft&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;draftMode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;/code&gt;&lt;/pre&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;// A persistent, visible banner whenever draft mode is active in the current browser&lt;/span&gt;
&lt;span class="c1"&gt;// app/layout.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;draftMode&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isEnabled&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="nf"&gt;draftMode&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="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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isEnabled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="na"&gt;style&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="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#facc15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;textAlign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&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;
            Draft mode is on in this browser.&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&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;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/api/disable-draft"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Exit preview&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&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="si"&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;A persistent, visible banner, shown for as long as draft mode remains enabled in that specific browser, with a clear, one-click way to exit it, closes most of the actual confusion here. It makes the scoped, per-browser nature of draft mode visible and obvious in the moment, rather than an invisible state someone has to remember exists and remember to manually undo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Connects Back to the Original Setup
&lt;/h2&gt;

&lt;p&gt;This banner pattern is worth adding as a standard part of any draft mode implementation, not an optional extra. Without it, draft mode functions correctly but silently, and silent, correctly-functioning features that contradict someone's natural mental model of how they work are exactly the kind of thing that causes recurring, hard-to-diagnose confusion, not because anything's actually broken, but because nothing visible corrects the wrong assumption in the moment it matters.&lt;/p&gt;

&lt;p&gt;I build this banner pattern into the CMS setups I put together for client projects and the templates at &lt;a href="https://www.pixelanas.com" rel="noopener noreferrer"&gt;pixelanas.com&lt;/a&gt;, specifically because the confusion this prevents is common enough to be worth the small amount of extra code every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Draft mode is a per-browser, cookie-scoped state, not a global site toggle, and that gap between actual behavior and intuitive assumption is exactly what causes real confusion.&lt;/strong&gt; Pairing every draft mode implementation with a persistent, visible indicator and an obvious way to exit closes that gap directly, rather than leaving editors to remember and manually track an invisible state themselves.&lt;/p&gt;




&lt;p&gt;If you've implemented draft mode without a visible indicator showing when it's active, worth adding one, this is a small addition that prevents a genuinely common, confusing situation. Drop your own experience with this in the comments, curious whether this confusion is as common elsewhere as it's been on projects I've worked on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>cms</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Binding Extra Arguments to a Server Action? The Order Matters More Than You'd Think</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:15:47 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/binding-extra-arguments-to-a-server-action-the-order-matters-more-than-youd-think-334n</link>
      <guid>https://dev.to/anas_sheikh_2/binding-extra-arguments-to-a-server-action-the-order-matters-more-than-youd-think-334n</guid>
      <description>&lt;p&gt;This is a small, easy detail to get backwards, and getting it backwards doesn't throw a clear error, it just means one of your two values silently isn't what you expected it to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Common Pattern This Applies To
&lt;/h2&gt;

&lt;p&gt;A Server Action used directly as a form's &lt;code&gt;action&lt;/code&gt; prop automatically receives &lt;code&gt;FormData&lt;/code&gt; as its argument. Sometimes you also need to pass something extra, an ID identifying which specific record this particular form instance belongs to, that isn't itself a field in the form.&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;// components/DeletePostButton.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;deletePost&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;@/actions/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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DeletePostButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;postId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;postId&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;deletePost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&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="nx"&gt;postId&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;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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;form&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// actions/posts.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;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;deletePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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="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;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByIdAndDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard/posts&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;This is the correct, standard pattern, and it hinges entirely on getting the parameter order right in both places, matching each other exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Actually Goes Wrong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.bind(null, postId)&lt;/code&gt; pre-fills the function's arguments starting from the left, the first parameter position, not appended after whatever Next.js would normally pass in. &lt;code&gt;postId&lt;/code&gt; becomes the function's first argument, and &lt;code&gt;formData&lt;/code&gt;, which Next.js still automatically supplies, becomes the second, shifted over by exactly one position.&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;// ❌ Function signature doesn't match the actual argument order .bind() produces&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;deletePost&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="nx"&gt;postId&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="c1"&gt;// formData here is actually receiving postId's value&lt;/span&gt;
  &lt;span class="c1"&gt;// postId here is actually receiving formData&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="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// logs a FormData object, not the string you expected&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap the parameter order in the function signature so it doesn't match how &lt;code&gt;.bind()&lt;/code&gt; actually supplies arguments, and both values silently land in the wrong place. No error gets thrown, since both parameters exist and TypeScript, unless you're being genuinely careful with the types here, often won't catch this either, since &lt;code&gt;FormData&lt;/code&gt; and a bound &lt;code&gt;string&lt;/code&gt; are both just values being passed to a function expecting some combination of two arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Specific Mistake Is Easy to Make
&lt;/h2&gt;

&lt;p&gt;The natural, intuitive mental model is "the form gives me formData, and I'm adding an extra value on top of that," which suggests formData first, extra value second, matching the order you'd think about it in conversation. &lt;code&gt;.bind()&lt;/code&gt;'s actual behavior, prepending arguments from the left, runs counter to that intuition, and the correct order, bound value first, formData second, only becomes obvious once you specifically know how &lt;code&gt;.bind()&lt;/code&gt; works, not from how the situation naturally gets described out loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Actually Manifests as a Bug
&lt;/h2&gt;

&lt;p&gt;Depending on what your function does with each parameter, this can fail in different ways. If &lt;code&gt;postId&lt;/code&gt; is used directly as a database ID and it's actually receiving a &lt;code&gt;FormData&lt;/code&gt; object instead, a database query with a malformed ID typically does throw a real, if somewhat confusing, error, which at least surfaces the problem, even if not obviously. If the mismatched values happen to both be used in ways that don't immediately throw, string interpolation, a loose comparison, the bug can produce quietly wrong behavior with no error at all, which is the more dangerous version, since nothing points you toward the actual cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Match the Order Deliberately, and Consider Typing It Explicitly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// actions/posts.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;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;deletePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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="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;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByIdAndDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard/posts&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;Keeping the function signature's argument order matching exactly how &lt;code&gt;.bind()&lt;/code&gt; supplies them, bound values first, in the order they were bound, &lt;code&gt;formData&lt;/code&gt; last, is the actual fix. For extra safety, especially on a function you're not confident everyone touching the codebase will get right by memory, a comment directly above the signature noting the expected call pattern removes any ambiguity for the next person editing it.&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;// Called as: deletePost.bind(null, postId) — postId first, formData supplied automatically after&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;deletePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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="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="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Binding Multiple Extra Arguments
&lt;/h2&gt;

&lt;p&gt;The same left-to-right rule extends cleanly to more than one bound value, in the exact order they're passed to &lt;code&gt;.bind()&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;updatePost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&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="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentUserId&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;/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="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;updatePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&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="nx"&gt;userId&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="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="c1"&gt;// postId first, userId second, formData last, matching the bind() call exactly&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.bind()&lt;/code&gt; prepends arguments from the left, in the order you pass them, and whatever Next.js automatically supplies, &lt;code&gt;formData&lt;/code&gt; for a form action, always ends up last, after every explicitly bound value.&lt;/strong&gt; The function signature needs to match that exact order, not the order that feels most natural to describe the situation in conversation. When in doubt, a one-line comment above the function noting the expected &lt;code&gt;.bind()&lt;/code&gt; call pattern is cheap insurance against exactly this kind of easy-to-miss, hard-to-notice mistake.&lt;/p&gt;

&lt;p&gt;I run into this pattern constantly building the SaaS dashboards and templates I sell at &lt;a href="https://www.pixelanas.com" rel="noopener noreferrer"&gt;pixelanas.com&lt;/a&gt;, and it's exactly the kind of small detail worth getting right once and documenting, rather than re-deriving from memory every time a new form needs an extra bound argument.&lt;/p&gt;




&lt;p&gt;If you've got a Server Action bound with extra arguments, worth double-checking the parameter order actually matches, especially on anything where a mismatch wouldn't throw an obvious error. Drop what you find in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>react</category>
    </item>
    <item>
      <title>Why Your Next.js Modal Route Works Perfectly Until Someone Refreshes the Page</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Mon, 21 Sep 2026 11:18:57 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/why-your-nextjs-modal-route-works-perfectly-until-someone-refreshes-the-page-17ne</link>
      <guid>https://dev.to/anas_sheikh_2/why-your-nextjs-modal-route-works-perfectly-until-someone-refreshes-the-page-17ne</guid>
      <description>&lt;p&gt;This one isn't really a bug, it's a genuinely common point of confusion about a feature working exactly as designed, and understanding why it behaves this way changes how you'd actually build around it, rather than fighting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern: A Photo Grid That Opens Items as a Modal
&lt;/h2&gt;

&lt;p&gt;Intercepting routes are what makes an Instagram-style interaction possible in the App Router, click a photo in a grid, it opens as a modal overlay on top of the current page, without a full navigation, while the URL still updates to something shareable and bookmarkable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── @modal/
│   ├── default.tsx
│   └── (.)photo/
│       └── [id]/
│           └── page.tsx    // renders as a modal when intercepted
├── photo/
│   └── [id]/
│       └── page.tsx        // the actual full page
├── layout.tsx
└── page.tsx                 // the grid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click a photo from the grid, and the &lt;code&gt;(.)photo/[id]&lt;/code&gt; route intercepts the navigation, rendering the modal version in the &lt;code&gt;@modal&lt;/code&gt; slot on top of the grid, without a full page transition. The URL correctly updates to &lt;code&gt;/photo/123&lt;/code&gt;. It feels seamless, and it's a genuinely well-built pattern for exactly this kind of interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Confusion Starts
&lt;/h2&gt;

&lt;p&gt;Someone building this, testing by clicking through the grid, sees exactly the intended modal behavior. Then they refresh the page while the modal is open, or share that &lt;code&gt;/photo/123&lt;/code&gt; URL with someone else, or that someone else pastes it directly into a new browser tab, and instead of the modal, they get the full, standalone &lt;code&gt;photo/[id]/page.tsx&lt;/code&gt; page, no grid behind it, no modal chrome, just the plain page rendering on its own.&lt;/p&gt;

&lt;p&gt;The natural first reaction is that something's broken, the modal "isn't working" on refresh. It's actually working exactly as designed, just not in the way that first reaction assumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Correct, Not Broken
&lt;/h2&gt;

&lt;p&gt;Interception is specifically a client-side navigation behavior. It only intercepts a navigation that happens through Next.js's client-side router, clicking a &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt;, calling &lt;code&gt;router.push()&lt;/code&gt;, moving from one already-loaded page to another within the same app session. A hard refresh, a direct URL visit, or someone opening that link in a completely fresh browser tab isn't a client-side navigation at all, it's a fresh, full server request for that specific URL, with no prior page state to intercept from, no grid already rendered behind it to interrupt. Next.js correctly falls back to rendering the actual underlying route, the full page, because that's genuinely the only thing that makes sense in a context where there's no previous client-side navigation to interrupt in the first place.&lt;/p&gt;

&lt;p&gt;This is why the folder structure includes both the intercepted route (&lt;code&gt;(.)photo/[id]&lt;/code&gt;, the modal version) and the actual route (&lt;code&gt;photo/[id]&lt;/code&gt;, the full page) as separate, real routes. The full page isn't a fallback or an error state, it's the intended, correct experience for anyone arriving at that URL directly, someone who shared or bookmarked the link, a search engine crawling it, anyone without an existing client-side session to interrupt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Design Implication
&lt;/h2&gt;

&lt;p&gt;This isn't something to work around, it's something to design for deliberately. The full, non-modal version of the route needs to be a genuinely complete, standalone page on its own merits, not a stripped-down fallback that only half-works, since a real share of your actual traffic to that URL, direct visits, refreshes, shared links, search engines, will land there specifically, not on the modal version.&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/photo/[id]/page.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// This needs to be a genuinely complete page, not an afterthought,&lt;/span&gt;
&lt;span class="c1"&gt;// since direct visits, refreshes, and shared links all land here specifically&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;PhotoPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&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;id&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;params&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;photo&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;getPhoto&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="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="nc"&gt;BackToGridLink&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;/* since there's no grid rendered behind this version */&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;PhotoDisplay&lt;/span&gt; &lt;span class="na"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;photo&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;PhotoMetadata&lt;/span&gt; &lt;span class="na"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;photo&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Including something like a clear way back to the grid, since this version genuinely doesn't have the grid rendered behind it the way the modal does, treats this as the real, complete experience it needs to be for a meaningful share of actual visitors, rather than assuming everyone always arrives via the modal path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Actually Matters for SEO Too
&lt;/h2&gt;

&lt;p&gt;This split has a genuine upside worth being deliberate about. Since the full page is a real, standalone route, it's exactly what search engines actually crawl and index, a real, complete, server-rendered page for that specific photo, with its own metadata, its own content, rather than something only reachable through a client-side modal interaction a crawler would never trigger. Building the full page version to be genuinely complete isn't just handling an edge case gracefully, it's the version doing real SEO work for that specific piece of content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Intercepting routes are a client-side navigation enhancement layered on top of real, independently functional routes, not a replacement for them.&lt;/strong&gt; The full, non-intercepted version of the route needs to be built as a genuinely complete experience on its own, since refreshes, direct visits, shared links, and search engine crawlers all land there specifically, not on the modal version, and treating it as an afterthought means a real share of your actual traffic gets an incomplete experience.&lt;/p&gt;




&lt;p&gt;If you're building or have built an intercepting route pattern, worth checking whether the full, non-modal version genuinely stands on its own, or was built assuming everyone always arrives through the modal. Drop your experience with this pattern in the comments, curious how many people hit this exact confusion the first time before understanding why it's actually correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>That bcrypt.hash(password, 10) You Copy-Pasted Might Not Be as Secure as You Think</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Sun, 20 Sep 2026 09:48:24 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/that-bcrypthashpassword-10-you-copy-pasted-might-not-be-as-secure-as-you-think-1jei</link>
      <guid>https://dev.to/anas_sheikh_2/that-bcrypthashpassword-10-you-copy-pasted-might-not-be-as-secure-as-you-think-1jei</guid>
      <description>&lt;p&gt;Almost every auth tutorial, including some of my own examples in earlier posts, uses &lt;code&gt;bcrypt.hash(password, 10)&lt;/code&gt; without much explanation of what that number actually controls or why it's 10 specifically rather than some other value. It's worth understanding, because the right number isn't fixed, it's a moving target that most codebases never revisit after the initial copy-paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Salt Rounds Number Actually Controls
&lt;/h2&gt;

&lt;p&gt;bcrypt's cost factor, the second argument, controls how computationally expensive hashing a single password is, specifically by determining how many times an internal key-setup routine repeats. It's not a linear scale, each increment roughly doubles the computation required. A cost factor of 10 means roughly 1,024 iterations of that internal routine. 11 means roughly 2,048. 12 means roughly 4,096.&lt;/p&gt;

&lt;p&gt;This deliberate slowness is the entire point. A fast hash function is great for most use cases and terrible for passwords specifically, since a fast hash lets an attacker who's obtained a database of hashed passwords try billions of guesses per second against them. A deliberately slow hash function directly limits how many guesses an attacker can attempt in any given amount of time, even with significant computing power.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 10 Specifically, and Why That's Worth Questioning
&lt;/h2&gt;

&lt;p&gt;The number 10 shows up constantly in tutorials and documentation because it was, at the time much of that content was written, a reasonable balance, slow enough to meaningfully limit brute-force attempts, fast enough not to noticeably slow down a real login flow for a legitimate user. That balance point depends entirely on how fast the hardware attempting to brute-force it actually is, and hardware, particularly specialized hardware built for exactly this kind of computation, gets faster every year. A cost factor considered a reasonable balance several years ago represents meaningfully less real protection against modern hardware than it did when that number first became the common default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Doesn't Show Up as an Obvious Problem
&lt;/h2&gt;

&lt;p&gt;There's no error, no warning, no visible symptom of a salt rounds value that's become outdated relative to current hardware capability. Login continues working exactly as expected for legitimate users. The gap is entirely about resistance to an offline brute-force attempt against a stolen password database, something that only becomes relevant if that database is ever actually compromised, which means the weakness sits completely invisible until the exact moment it matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Guidance, and Why It's a Moving Target Rather Than a Fixed Number
&lt;/h2&gt;

&lt;p&gt;Security guidance on this specific number gets revised periodically as hardware capability changes, and current recommendations from security-focused organizations trend higher than the value that shows up in most existing tutorials and codebases. Rather than citing a specific number here that will itself become outdated, the actual, durable guidance is this, check current recommendations specifically when setting this up, rather than trusting whatever number happens to be sitting in the tutorial or codebase you're referencing, since that number is a snapshot of a past moment, not a permanently correct value.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Way to Choose the Right Number for Your Own Server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A quick, practical way to find a cost factor appropriate for your actual hardware,&lt;/span&gt;
&lt;span class="c1"&gt;// rather than trusting a number copied from somewhere else&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&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;bcryptjs&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;benchmarkCostFactor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;start&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;await&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;benchmark-password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rounds&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;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;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Run this on your actual production server or equivalent hardware,&lt;/span&gt;
&lt;span class="c1"&gt;// and pick the highest cost factor where a single hash still completes&lt;/span&gt;
&lt;span class="c1"&gt;// in a reasonable time for your login flow, commonly cited targets&lt;/span&gt;
&lt;span class="c1"&gt;// are in the range of a few hundred milliseconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a real, current answer specific to your actual infrastructure, rather than a number that was correct for someone else's hardware at some point in the past and has simply been carried forward through tutorials and copy-paste ever since.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader Rule This Points To
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A security parameter borrowed from a tutorial is a snapshot of what was reasonable when that tutorial was written, not a permanently correct value.&lt;/strong&gt; This applies beyond just bcrypt cost factors, TLS cipher suite choices, JWT expiration windows, rate limit thresholds, all of these are calibrated against assumptions, about hardware, about attacker capability, about acceptable risk, that shift over time. A value copied once and never revisited quietly drifts from "reasonable" toward "outdated" without any code change ever signaling that drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Actually Do About Your Own Codebase
&lt;/h2&gt;

&lt;p&gt;Check what cost factor your app is actually using right now, and compare it against current guidance rather than assuming a number from an older tutorial or your own original setup is still appropriate. If it's genuinely outdated, increasing it is straightforward for new passwords going forward, though existing stored hashes were created with the old cost factor and remain at that lower value unless you specifically implement a rehash-on-next-login pattern, checking and upgrading a user's stored hash cost factor the next time they successfully log in with their correct password.&lt;/p&gt;




&lt;p&gt;Check what cost factor your own app is actually using, and when you last revisited that number versus just inheriting it from the original setup or a tutorial. Drop what you find in the comments, genuinely curious how many people have actually reconsidered this since first setting it up versus never touching it again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>security</category>
      <category>auth</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Optional Chaining Is Probably Hiding Real Bugs in Your Next.js App</title>
      <dc:creator>Anas Sheikh</dc:creator>
      <pubDate>Sat, 19 Sep 2026 10:55:36 +0000</pubDate>
      <link>https://dev.to/anas_sheikh_2/optional-chaining-is-probably-hiding-real-bugs-in-your-nextjs-app-4c0</link>
      <guid>https://dev.to/anas_sheikh_2/optional-chaining-is-probably-hiding-real-bugs-in-your-nextjs-app-4c0</guid>
      <description>&lt;p&gt;Optional chaining is genuinely useful for values that are legitimately, expectedly sometimes absent. The problem is how often it gets applied defensively, everywhere, as a reflex against crashes, including on values that should always exist if everything upstream is actually working correctly. When one of those genuinely-should-never-be-undefined values is undefined anyway, &lt;code&gt;?.&lt;/code&gt; doesn't surface that as the real problem it is, it just quietly renders nothing and moves on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern That Looks Like Careful, Defensive Code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&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;DashboardPage&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;session&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;getSession&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;user&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;getUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userId&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;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Welcome, &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&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;h1&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;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Role: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&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;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdminPanel&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of these optional chains looks like responsible, defensive coding. If &lt;code&gt;session&lt;/code&gt; is somehow null, &lt;code&gt;session?.userId&lt;/code&gt; gracefully becomes &lt;code&gt;undefined&lt;/code&gt; instead of throwing. If &lt;code&gt;user&lt;/code&gt; fails to load, the page still renders instead of crashing. This feels careful. It's also actively hiding the fact that, if this route is only reachable by an authenticated user in the first place, &lt;code&gt;session&lt;/code&gt; and &lt;code&gt;user&lt;/code&gt; being undefined here isn't a normal, expected condition, it's a sign something upstream is genuinely broken, an auth check that should have redirected but didn't, a database lookup silently failing, a race condition in how the session gets set. None of that gets surfaced. The page just quietly shows "Welcome, " with a blank name and no admin panel, and looks like a minor rendering glitch instead of the real bug it actually is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Worse Than It Sounds for the Admin Check Specifically
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdminPanel&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line deserves particular attention. If &lt;code&gt;user&lt;/code&gt; is unexpectedly &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;user?.role&lt;/code&gt; evaluates to &lt;code&gt;undefined&lt;/code&gt;, the comparison to &lt;code&gt;'admin'&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, and the admin panel correctly doesn't render. That specific outcome is safe, a broken session fails closed here, not open. But notice what's actually happening, a genuinely serious bug, the current user's identity failing to load at all, on a page that's supposed to require authentication, is being silently absorbed into the exact same code path as "this user is correctly not an admin." Both produce identical, unremarkable output. One of them is completely normal. The other represents your auth system genuinely malfunctioning, and nothing distinguishes them from each other anywhere in this code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Distinction Worth Making Deliberately
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Values that are legitimately, expectedly sometimes absent&lt;/strong&gt;, an optional profile bio, an optional avatar image, a field that's genuinely allowed to not exist as part of normal, correct behavior. Optional chaining is exactly the right tool here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Values that should always exist if everything upstream actually worked correctly&lt;/strong&gt;, the current user on an authenticated route, a database record that was just confirmed to exist moments earlier, a required field on a validated form submission. Optional chaining on these doesn't handle an edge case gracefully, it hides a real failure by making it look identical to a normal, unremarkable state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do Instead for the Second Category
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&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;DashboardPage&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;session&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;getSession&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;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// handle the genuinely expected "not logged in" case explicitly&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;user&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;getUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// session exists but the user record doesn't, this is a real, unexpected problem&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Session exists but user not found:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User data could not be loaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// let it surface, don't hide it&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;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Welcome, &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;h1&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;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Role: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&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;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AdminPanel&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No optional chaining needed anywhere past this point, because both genuinely expected absence cases, no session, get handled explicitly, with a real redirect and a real, loud error respectively, rather than silently smoothed over. Everything after those two checks can safely assume &lt;code&gt;session&lt;/code&gt; and &lt;code&gt;user&lt;/code&gt; are real, present values, because any case where they aren't has already been caught and surfaced, not quietly absorbed into a blank render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Optional chaining should be a deliberate choice for values that are genuinely, normally allowed to be absent, not a reflexive habit applied to anything that might theoretically be undefined.&lt;/strong&gt; For a value that represents a genuine invariant, something that should always be true if the rest of the system is working correctly, handle its absence explicitly, a redirect, a thrown error, a logged warning, something that actually surfaces the problem, rather than a &lt;code&gt;?.&lt;/code&gt; that quietly renders nothing and leaves the real bug invisible until someone notices the symptom much later, disconnected from its actual cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Way to Audit Your Own Code
&lt;/h2&gt;

&lt;p&gt;For every &lt;code&gt;?.&lt;/code&gt; in a codebase, ask honestly, is this value legitimately allowed to be missing as part of normal, correct behavior, or would its absence here actually indicate something upstream is broken. The first case is optional chaining used correctly. The second is optional chaining quietly doing the opposite of what defensive code is supposed to do, hiding a real failure instead of catching it.&lt;/p&gt;




&lt;p&gt;Go find the optional chains in your own codebase specifically sitting on values you'd genuinely be alarmed to discover were undefined, an authenticated user, a record that was just confirmed to exist. If any of those chains are just gracefully rendering blank instead of surfacing a real problem, that's worth fixing. Drop what you find in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a href="https://pixelanas.gumroad.com" rel="noopener noreferrer"&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a href="https://x.com/ASheikh69751" rel="noopener noreferrer"&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>react</category>
    </item>
  </channel>
</rss>
