<?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: Ahmed Mahmoud</title>
    <description>The latest articles on DEV Community by Ahmed Mahmoud (@ahmed_mahmoud360).</description>
    <link>https://dev.to/ahmed_mahmoud360</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%2F656404%2F01b9474b-ca4f-4578-a15e-36a90ad96c82.jpeg</url>
      <title>DEV Community: Ahmed Mahmoud</title>
      <link>https://dev.to/ahmed_mahmoud360</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmed_mahmoud360"/>
    <language>en</language>
    <item>
      <title>Route Handlers vs Server Actions in Next.js: When I Reach for Each</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Tue, 21 Jul 2026 06:14:51 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/route-handlers-vs-server-actions-in-nextjs-when-i-reach-for-each-i5m</link>
      <guid>https://dev.to/ahmed_mahmoud360/route-handlers-vs-server-actions-in-nextjs-when-i-reach-for-each-i5m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; In the Next.js App Router, a Server Action is an async function you call directly from a component to run a mutation, and a Route Handler is a standard HTTP endpoint at &lt;code&gt;app/api/**/route.ts&lt;/code&gt;. Reach for a Server Action for mutations driven by your own React UI; reach for a Route Handler for public APIs, webhooks, and streaming. Both compile to publicly callable endpoints, so authenticate inside each one.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Server Action&lt;/strong&gt; is an async function marked &lt;code&gt;'use server'&lt;/code&gt; that Next.js exposes as an internal POST endpoint you invoke directly from a component — usually a form &lt;code&gt;action&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Route Handler&lt;/strong&gt; is a function exported from &lt;code&gt;app/api/**/route.ts&lt;/code&gt; named after an HTTP method that receives a Web &lt;code&gt;Request&lt;/code&gt; and returns a Web &lt;code&gt;Response&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;Server Action&lt;/strong&gt; for mutations from your own UI — progressive enhancement plus &lt;code&gt;revalidateTag&lt;/code&gt; in the same round trip.&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;Route Handler&lt;/strong&gt; for public APIs, webhooks, OAuth callbacks, cacheable GET endpoints, and streaming.&lt;/li&gt;
&lt;li&gt;Both are &lt;strong&gt;public endpoints&lt;/strong&gt; — authenticate, authorize, and validate inputs inside every one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is a Server Action vs a Route Handler in Next.js?
&lt;/h2&gt;

&lt;p&gt;A Server Action is an async function marked with the &lt;code&gt;'use server'&lt;/code&gt; directive that Next.js compiles into an RPC-style endpoint. You import it and call it like a normal function — most often as the &lt;code&gt;action&lt;/code&gt; of a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; — and Next.js serializes the call over a POST request behind the scenes.&lt;/p&gt;

&lt;p&gt;A Route Handler is a function exported from a &lt;code&gt;route.ts&lt;/code&gt; file inside &lt;code&gt;app/&lt;/code&gt;, named after the HTTP method it serves (&lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;). It receives a standard Web &lt;code&gt;Request&lt;/code&gt; and returns a standard Web &lt;code&gt;Response&lt;/code&gt;, so you control the status code, headers, and body directly. It replaces &lt;code&gt;pages/api&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should I use a Server Action instead of a Route Handler?
&lt;/h2&gt;

&lt;p&gt;Use a Server Action when the mutation is triggered from your own React UI and has no external consumer. A form that updates a profile, a button that deletes a row — these belong in Server Actions because the action lives next to the component that calls it.&lt;/p&gt;

&lt;p&gt;Server Actions give you progressive enhancement (a &lt;code&gt;&amp;lt;form action={fn}&amp;gt;&lt;/code&gt; works before JS hydrates) and in-place cache revalidation (&lt;code&gt;revalidateTag&lt;/code&gt; re-renders affected Server Components in the same round trip).&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/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&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;@/lib/auth&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;updateName&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="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;auth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                 &lt;span class="c1"&gt;// authenticate INSIDE the action&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="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;Unauthorized&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&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="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;name&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="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&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;name&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="na"&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;Name is required&lt;/span&gt;&lt;span class="dl"&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&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;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When should I use a Route Handler instead of a Server Action?
&lt;/h2&gt;

&lt;p&gt;Use a Route Handler when something outside your React tree calls the endpoint over a stable URL: webhook receivers (Stripe, GitHub), OAuth callbacks, mobile or third-party API consumers, cron jobs, and cacheable GET endpoints.&lt;/p&gt;

&lt;p&gt;Route Handlers are also the only option for raw request access or streaming. A Stripe webhook needs the unparsed body to verify the signature; an SSE feed or LLM token stream returns a &lt;code&gt;ReadableStream&lt;/code&gt;. Server Actions return a single serialized value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/webhooks/stripe/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;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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stripe&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;@/lib/stripe&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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;body&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                        &lt;span class="c1"&gt;// raw body for signature check&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&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;stripe-signature&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="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WH_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid signature&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do Route Handlers and Server Actions compare?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Server Action&lt;/th&gt;
&lt;th&gt;Route Handler&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How it's called&lt;/td&gt;
&lt;td&gt;Imported and called like a function&lt;/td&gt;
&lt;td&gt;HTTP request to a URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP method&lt;/td&gt;
&lt;td&gt;Always POST (internally)&lt;/td&gt;
&lt;td&gt;Any (GET/POST/PUT/PATCH/DELETE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External clients&lt;/td&gt;
&lt;td&gt;No stable public contract&lt;/td&gt;
&lt;td&gt;Yes — stable URL + method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Progressive enhancement&lt;/td&gt;
&lt;td&gt;Yes (form works without JS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cacheable GET&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;Cache-Control&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;ReadableStream&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache revalidation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;revalidateTag&lt;/code&gt; inline&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Mutations from your own UI&lt;/td&gt;
&lt;td&gt;Public APIs, webhooks, streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Do I need a Route Handler to fetch data for my own page?
&lt;/h2&gt;

&lt;p&gt;No. Inside a Server Component you query your database directly — no Route Handler required. Wrapping the query in &lt;code&gt;app/api&lt;/code&gt; and fetching it from the same server just adds an HTTP round trip and a second cache layer for no benefit.&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/dashboard/page.tsx — no Route Handler, no fetch()&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;Page&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;stats&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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="nc"&gt;Stats&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;stats&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;Reserve Route Handler &lt;code&gt;GET&lt;/code&gt;s for data a &lt;em&gt;different&lt;/em&gt; client needs: a public JSON API, an edge-cached response, or an endpoint a mobile app calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are Server Actions and Route Handlers both public endpoints?
&lt;/h2&gt;

&lt;p&gt;Yes — both are publicly reachable POST endpoints. A Server Action is not private just because you call it from a component and never see its URL. Next.js compiles each action into an endpoint with an unguessable ID, but that ID can be invoked directly with a crafted request. The action ID is obscurity, not authorization.&lt;/p&gt;

&lt;p&gt;Treat every Server Action and Route Handler as an untrusted entry point: authenticate the caller, check authorization, and validate every input inside the function body. The client UI enforces nothing.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q: Can a Server Action handle a GET request?&lt;/strong&gt;&lt;br&gt;
A: No. Server Actions always execute as POST. For a cacheable GET endpoint, use a Route Handler exporting a &lt;code&gt;GET&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I use a Route Handler to fetch data for my own page?&lt;/strong&gt;&lt;br&gt;
A: No. In a Server Component, query your data source directly. A Route Handler adds an extra HTTP round trip and cache layer for no benefit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Are Server Actions secure by default because they have no visible URL?&lt;/strong&gt;&lt;br&gt;
A: No. Every Server Action compiles to a callable POST endpoint. Authenticate, authorize, and validate inputs inside the action itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Which one supports streaming responses?&lt;/strong&gt;&lt;br&gt;
A: Route Handlers. Return a &lt;code&gt;ReadableStream&lt;/code&gt; for SSE or LLM token streams. A Server Action returns one serialized result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can a mobile app or third-party service call my Server Actions?&lt;/strong&gt;&lt;br&gt;
A: Not reliably. Server Action IDs are generated at build time and are not a stable public contract. Expose a Route Handler with a versioned URL.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/nextjs-route-handlers-vs-server-actions-field-notes" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/nextjs-route-handlers-vs-server-actions-field-notes" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>URL State in the Next.js App Router: How I Stopped Losing UI State on Refresh</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:28:25 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/url-state-in-the-nextjs-app-router-how-i-stopped-losing-ui-state-on-refresh-1bk0</link>
      <guid>https://dev.to/ahmed_mahmoud360/url-state-in-the-nextjs-app-router-how-i-stopped-losing-ui-state-on-refresh-1bk0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; URL state means storing UI state — filters, active tab, search query, pagination — in the URL query string instead of React &lt;code&gt;useState&lt;/code&gt;, so a refresh, a shared link, or the back button restores exactly what the user saw. In the Next.js App Router I read it with &lt;code&gt;useSearchParams()&lt;/code&gt; or the async &lt;code&gt;searchParams&lt;/code&gt; prop, write it with &lt;code&gt;router.replace()&lt;/code&gt;, and reach for &lt;code&gt;nuqs&lt;/code&gt; when I want a type-safe, &lt;code&gt;useState&lt;/code&gt;-shaped API. The trap that cost me an afternoon: a component calling &lt;code&gt;useSearchParams()&lt;/code&gt; must sit inside a React &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary or the production build fails.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A dashboard I built kept its filter panel in &lt;code&gt;useState&lt;/code&gt;. Users would filter to a subset, refresh to pull fresh data, and lose every selection. The fix was not more state management — it was less: move the state a user expects to survive a reload into the URL, where the browser persists it for free.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;URL state stores UI state in the URL query string, so a refresh, a shared link, or the back button restores what the user saw; &lt;code&gt;useState&lt;/code&gt; is lost on every reload.&lt;/li&gt;
&lt;li&gt;Read query state with &lt;code&gt;useSearchParams()&lt;/code&gt; in a client component, or the async &lt;code&gt;searchParams&lt;/code&gt; prop on a server component (a Promise you &lt;code&gt;await&lt;/code&gt; in Next.js 15).&lt;/li&gt;
&lt;li&gt;Write it with &lt;code&gt;router.replace()&lt;/code&gt; and a &lt;code&gt;URLSearchParams&lt;/code&gt; object; use &lt;code&gt;replace&lt;/code&gt; not &lt;code&gt;push&lt;/code&gt; so filter tweaks do not flood the back-button history.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;nuqs&lt;/code&gt; library gives a &lt;code&gt;useState&lt;/code&gt;-shaped hook, &lt;code&gt;useQueryState&lt;/code&gt;, with typed parsers like &lt;code&gt;parseAsInteger&lt;/code&gt; instead of raw strings.&lt;/li&gt;
&lt;li&gt;A component calling &lt;code&gt;useSearchParams()&lt;/code&gt; must be wrapped in &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; during static rendering, or the Next.js build errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When should I store state in the URL instead of useState?
&lt;/h2&gt;

&lt;p&gt;Store state in the URL when the user expects it to survive a refresh, a shared link, or a bookmark — filters, the active tab, a search term, sort order, and pagination all qualify. Keep state in &lt;code&gt;useState&lt;/code&gt; when it is ephemeral: a hover state, whether a dropdown is open, an unsaved form draft. My test is one question: if the user copies this URL and sends it to a colleague, should the colleague see the same view? If yes, it belongs in the URL. URL state also kills a class of sync bugs — there is one source of truth, the query string, instead of a &lt;code&gt;useState&lt;/code&gt; value that drifts from the address bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I read search params in the Next.js App Router?
&lt;/h2&gt;

&lt;p&gt;In a client component, call &lt;code&gt;useSearchParams()&lt;/code&gt; from &lt;code&gt;next/navigation&lt;/code&gt;, which returns a read-only &lt;code&gt;URLSearchParams&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="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;useSearchParams&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;StatusBadge&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;searchParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearchParams&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&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;status&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&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;status&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;span&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;In a server component, skip the hook and read the &lt;code&gt;searchParams&lt;/code&gt; prop. In Next.js 15 it is a Promise, so you &lt;code&gt;await&lt;/code&gt; it and fetch the correct data before rendering.&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;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;Page&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;searchParams&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="nl"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;searchParams&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;rows&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;getRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&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="nc"&gt;Table&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&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;h2&gt;
  
  
  How do I write to the URL without triggering a full navigation?
&lt;/h2&gt;

&lt;p&gt;Build a fresh &lt;code&gt;URLSearchParams&lt;/code&gt; from the current params, set your key, and call &lt;code&gt;router.replace()&lt;/code&gt;. Seeding from the existing params preserves every other query key.&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="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;useRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usePathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSearchParams&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;StatusFilter&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePathname&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;searchParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearchParams&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setStatus&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="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="nx"&gt;params&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchParams&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="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;status&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="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&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="s2"&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;select&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;All&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;option&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;option&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;option&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;select&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;Use &lt;code&gt;router.replace()&lt;/code&gt;, not &lt;code&gt;router.push()&lt;/code&gt;. &lt;code&gt;push&lt;/code&gt; adds a history entry per change, so the back button steps through each tweak; &lt;code&gt;replace&lt;/code&gt; keeps one entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is nuqs, and what does useQueryState add?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;nuqs&lt;/code&gt; is a type-safe search-params state manager for React that turns the read-plus-&lt;code&gt;router.replace&lt;/code&gt; dance into one &lt;code&gt;useState&lt;/code&gt;-shaped hook. &lt;code&gt;useQueryState(key, parser)&lt;/code&gt; returns a &lt;code&gt;[value, setValue]&lt;/code&gt; tuple, but the value lives in the URL.&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="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;useQueryState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseAsInteger&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;nuqs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductSearch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;q&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;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseAsInteger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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;The parsers are the win: &lt;code&gt;parseAsInteger&lt;/code&gt;, &lt;code&gt;parseAsBoolean&lt;/code&gt;, &lt;code&gt;parseAsArrayOf&lt;/code&gt;, &lt;code&gt;parseAsStringEnum&lt;/code&gt;, and &lt;code&gt;parseAsIsoDateTime&lt;/code&gt; convert the URL string into the type your component wants and back on write. Setting a value to &lt;code&gt;null&lt;/code&gt; clears the key. In nuqs v2 you wrap the app once in &lt;code&gt;&amp;lt;NuqsAdapter&amp;gt;&lt;/code&gt; from &lt;code&gt;nuqs/adapters/next/app&lt;/code&gt;, and batch several keys with &lt;code&gt;useQueryStates&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native searchParams or nuqs — which should I pick?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Native useSearchParams&lt;/th&gt;
&lt;th&gt;nuqs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Types&lt;/td&gt;
&lt;td&gt;Strings only, parse yourself&lt;/td&gt;
&lt;td&gt;Typed parsers (int, bool, array, enum, date)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;NuqsAdapter&amp;gt;&lt;/code&gt; wrapper once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write API&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;URLSearchParams&lt;/code&gt; + &lt;code&gt;router.replace&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useState&lt;/code&gt;-like tuple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many keys&lt;/td&gt;
&lt;td&gt;Hand-built query string&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;useQueryStates&lt;/code&gt; batches them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defaults&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;?? fallback&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;withDefault&lt;/code&gt; + &lt;code&gt;clearOnDefault&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rapid writes&lt;/td&gt;
&lt;td&gt;Debounce yourself&lt;/td&gt;
&lt;td&gt;Built-in throttling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What breaks — Suspense, throttling, and stale server data?
&lt;/h2&gt;

&lt;p&gt;The build aborts with &lt;em&gt;"useSearchParams() should be wrapped in a suspense boundary"&lt;/em&gt;. During static rendering, any component reading &lt;code&gt;useSearchParams()&lt;/code&gt; must sit inside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary, because the params are only known at request time.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FiltersSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StatusFilter&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more edges. Writing to the URL on every keystroke spams history and re-renders — &lt;code&gt;nuqs&lt;/code&gt; throttles by default and exposes &lt;code&gt;throttleMs&lt;/code&gt;; with native code you debounce yourself. And whether a URL change re-runs server components depends on shallow routing: &lt;code&gt;nuqs&lt;/code&gt; defaults to &lt;code&gt;shallow: true&lt;/code&gt; (client-only URL update), so set &lt;code&gt;shallow: false&lt;/code&gt; when a server component filters through the &lt;code&gt;searchParams&lt;/code&gt; prop and needs the new value.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; When should I keep state in the URL versus useState?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; URL for state that should survive a refresh, shared link, or bookmark — filters, tabs, search, sort, pagination. &lt;code&gt;useState&lt;/code&gt; for ephemeral UI like open menus, hover, or an unsaved form draft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Do I need nuqs, or is useSearchParams enough?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; &lt;code&gt;useSearchParams&lt;/code&gt; plus &lt;code&gt;router.replace&lt;/code&gt; covers reading and writing. &lt;code&gt;nuqs&lt;/code&gt; adds typed parsers, a &lt;code&gt;useState&lt;/code&gt;-like API, multi-key batching, and throttling. One or two string params: native. Many typed params: nuqs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Why does my build fail with "useSearchParams() should be wrapped in a suspense boundary"?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; During static rendering Next.js requires any component calling &lt;code&gt;useSearchParams()&lt;/code&gt; to be inside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary, since the value is only known at request time. Wrap that component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Should I use router.push or router.replace for filter changes?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; &lt;code&gt;router.replace&lt;/code&gt;. &lt;code&gt;push&lt;/code&gt; adds a history entry per change, so the back button walks through every tweak; &lt;code&gt;replace&lt;/code&gt; keeps one entry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Does updating the URL re-run my server components?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; With native &lt;code&gt;router.replace&lt;/code&gt;, yes. With &lt;code&gt;nuqs&lt;/code&gt; it depends on the &lt;code&gt;shallow&lt;/code&gt; option: default &lt;code&gt;shallow: true&lt;/code&gt; is client-only, &lt;code&gt;shallow: false&lt;/code&gt; triggers the server refetch.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/url-state-nextjs-app-router-field-notes" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/url-state-nextjs-app-router-field-notes" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Error Handling in the Next.js App Router: How I Stopped One Throw From Blanking the Page</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Sun, 19 Jul 2026 15:48:47 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/error-handling-in-the-nextjs-app-router-how-i-stopped-one-throw-from-blanking-the-page-a3d</link>
      <guid>https://dev.to/ahmed_mahmoud360/error-handling-in-the-nextjs-app-router-how-i-stopped-one-throw-from-blanking-the-page-a3d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; In the Next.js App Router, &lt;code&gt;error.tsx&lt;/code&gt; is a React error boundary for a route segment: it catches errors thrown while rendering that segment and everything nested below it, hands you an &lt;code&gt;error&lt;/code&gt; object and a &lt;code&gt;reset()&lt;/code&gt; function, and must be a Client Component. It does not catch errors in its own layout, in event handlers, or in async code that runs outside render.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I lost an afternoon to a single unhandled throw that blanked an entire dashboard. One failing data call in a nested component took down the whole page instead of the one panel that broke. The App Router already had the tools to contain it — I just had the wrong mental model of where each error file sits and what it actually catches.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;error.tsx&lt;/code&gt; file in a route segment creates a React error boundary that catches errors thrown during rendering of that segment's page and any nested layouts, pages, and components below it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error.tsx&lt;/code&gt; must start with &lt;code&gt;'use client'&lt;/code&gt; because React error boundaries rely on class-component lifecycle methods that only run in the browser; the file receives two props, &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;reset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error.tsx&lt;/code&gt; cannot catch an error thrown by the layout in its own segment — that layout renders above the boundary, so you place an &lt;code&gt;error.tsx&lt;/code&gt; in the parent segment to catch it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;global-error.tsx&lt;/code&gt; is the only boundary that catches errors in the root layout; it replaces the root layout when active, so it must render its own &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, and it runs only in production.&lt;/li&gt;
&lt;li&gt;In production, Next.js strips the message from server-side errors and replaces it with a &lt;code&gt;digest&lt;/code&gt; hash so nothing leaks to the browser; the original error is logged on the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does error.tsx actually catch in the App Router?
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;error.tsx&lt;/code&gt; file catches errors thrown while rendering the route segment it lives in and everything nested below it. Drop it in a segment folder and Next.js automatically wraps that segment's &lt;code&gt;page.tsx&lt;/code&gt;, its nested layouts and pages, and every Server and Client Component they render in a React error boundary.&lt;/p&gt;

&lt;p&gt;What it does &lt;em&gt;not&lt;/em&gt; catch is the part that trips people up: errors thrown in event handlers like &lt;code&gt;onClick&lt;/code&gt;, errors thrown in async code that runs after render, a &lt;code&gt;notFound()&lt;/code&gt; call, and — the one that cost me time — an error thrown by the &lt;code&gt;layout.tsx&lt;/code&gt; or &lt;code&gt;template.tsx&lt;/code&gt; in the same segment.&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/dashboard/error.tsx catches throws from&lt;/span&gt;
&lt;span class="c1"&gt;// app/dashboard/page.tsx and anything nested under /dashboard,&lt;/span&gt;
&lt;span class="c1"&gt;// but NOT from app/dashboard/layout.tsx.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why must error.tsx be a Client Component?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;error.tsx&lt;/code&gt; must be a Client Component because React error boundaries are implemented with class-component lifecycle methods — &lt;code&gt;getDerivedStateFromError&lt;/code&gt; and &lt;code&gt;componentDidCatch&lt;/code&gt; — that only execute in the browser. It receives exactly two props: &lt;code&gt;error&lt;/code&gt;, an &lt;code&gt;Error&lt;/code&gt; instance that may carry a &lt;code&gt;digest&lt;/code&gt; string, and &lt;code&gt;reset&lt;/code&gt;, a zero-argument function that re-renders the boundary.&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="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;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;Error&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;reset&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="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alert"&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;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Something went wrong&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&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;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reset&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;Try again&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;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;h2&gt;
  
  
  When do I need global-error.tsx instead of error.tsx?
&lt;/h2&gt;

&lt;p&gt;You need &lt;code&gt;global-error.tsx&lt;/code&gt; when the error happens in the root layout or root template, which sit above every ordinary &lt;code&gt;error.tsx&lt;/code&gt;. A regular &lt;code&gt;error.tsx&lt;/code&gt; renders inside the layout it protects, so it can never replace a broken root layout. &lt;code&gt;global-error.tsx&lt;/code&gt; replaces the entire root layout when triggered — which is why it must include its own &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tags. It is active only in production; in development the Next.js error overlay takes over.&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="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;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;GlobalError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reset&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Application error&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&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;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reset&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;Reload&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;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;h2&gt;
  
  
  How do I handle "not found" separately from errors?
&lt;/h2&gt;

&lt;p&gt;Call &lt;code&gt;notFound()&lt;/code&gt; from &lt;code&gt;next/navigation&lt;/code&gt;, which throws a dedicated control-flow error caught by the nearest &lt;code&gt;not-found.tsx&lt;/code&gt;, not by &lt;code&gt;error.tsx&lt;/code&gt;. A missing record is an expected outcome, not a crash, so it deserves its own boundary and a real HTTP 404.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;notFound&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;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;Page&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="nx"&gt;post&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;getPost&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="nx"&gt;slug&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;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// renders the nearest not-found.tsx, sends HTTP 404&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;Article&lt;/span&gt; &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&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;h2&gt;
  
  
  Why are my server error messages replaced with a digest in production?
&lt;/h2&gt;

&lt;p&gt;Next.js deliberately strips the message and stack from errors thrown on the server in production and replaces them with a &lt;code&gt;digest&lt;/code&gt; property — an auto-generated hash — so it never leaks internals or secrets to the browser. The full error is logged on the server, and the &lt;code&gt;digest&lt;/code&gt; is what lets you match a user's report to that server log. In development you still see the real message. So inside a production &lt;code&gt;error.tsx&lt;/code&gt;, &lt;code&gt;error.message&lt;/code&gt; for a server error is generic — build your correlation around &lt;code&gt;error.digest&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I recover without a reload — and what about event-handler errors?
&lt;/h2&gt;

&lt;p&gt;Call the &lt;code&gt;reset()&lt;/code&gt; prop to recover without a full page reload; it asks React to re-render the boundary's contents by re-attempting the failed segment. If the cause was transient, the segment comes back with no navigation. For errors &lt;code&gt;error.tsx&lt;/code&gt; never sees — event handlers and async callbacks — catch them yourself and drive the UI with state.&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="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="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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SaveButton&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;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setErr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;            &lt;span class="c1"&gt;// a rejection here is NOT caught by error.tsx&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="nf"&gt;setErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// surface it with local state instead&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;err&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="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alert"&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;err&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Save&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;/&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;For expected failures in a Server Action, I return a typed error value rather than throwing, so the form renders the message inline without tripping an error boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The App Router error files at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;th&gt;Client Component?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Render errors in its segment and nested children&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not its own layout; receives &lt;code&gt;error&lt;/code&gt; + &lt;code&gt;reset&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;global-error.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Errors in the root layout or template&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Replaces the root layout; must render &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;; production only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not-found.tsx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;notFound()&lt;/code&gt; calls and unmatched routes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Returns HTTP 404; not triggered by real crashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event handlers / async&lt;/td&gt;
&lt;td&gt;None of the above&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;try/catch&lt;/code&gt; and surface with state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;strong&gt;Q: Does error.tsx catch errors in its own layout.tsx?&lt;/strong&gt;&lt;br&gt;
A: No. The layout renders above the boundary. To catch an error thrown by a segment's layout, put an &lt;code&gt;error.tsx&lt;/code&gt; in the parent segment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does my error.tsx not appear in development?&lt;/strong&gt;&lt;br&gt;
A: Next.js shows its development error overlay on top of your boundary, and &lt;code&gt;global-error.tsx&lt;/code&gt; only takes over in a production build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do error boundaries catch errors in onClick handlers?&lt;/strong&gt;&lt;br&gt;
A: No. React error boundaries only catch errors thrown during rendering. Wrap event-handler and async logic in &lt;code&gt;try/catch&lt;/code&gt; and surface the failure with state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the digest property on the error object?&lt;/strong&gt;&lt;br&gt;
A: A hash Next.js generates for server-side errors in production, replacing the real message so nothing leaks to the client. Match it against your server logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does calling notFound() trigger error.tsx?&lt;/strong&gt;&lt;br&gt;
A: No. &lt;code&gt;notFound()&lt;/code&gt; throws a dedicated control-flow signal caught by the nearest &lt;code&gt;not-found.tsx&lt;/code&gt; and returns a 404. &lt;code&gt;error.tsx&lt;/code&gt; is only for unexpected exceptions.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/nextjs-app-router-error-handling-field-notes" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/nextjs-app-router-error-handling-field-notes" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>View Transitions in Next.js: How I Added Animated Page Changes Without a Library</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Sat, 18 Jul 2026 07:23:35 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/view-transitions-in-nextjs-how-i-added-animated-page-changes-without-a-library-2a1a</link>
      <guid>https://dev.to/ahmed_mahmoud360/view-transitions-in-nextjs-how-i-added-animated-page-changes-without-a-library-2a1a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; The View Transitions API animates the change between two DOM states — you call &lt;code&gt;document.startViewTransition()&lt;/code&gt;, and the browser cross-fades or morphs the old page into the new one. In a Next.js App Router app I added smooth page and element transitions with a few lines of CSS and no animation library, and the only real trap was giving two elements the same &lt;code&gt;view-transition-name&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;The View Transitions API animates between two DOM states via &lt;code&gt;document.startViewTransition(updateDOM)&lt;/code&gt;; the browser snapshots the page before and after your callback and animates the difference.&lt;/li&gt;
&lt;li&gt;The default transition is a full-page cross-fade named &lt;code&gt;root&lt;/code&gt;; assign &lt;code&gt;view-transition-name&lt;/code&gt; to any element to animate it independently.&lt;/li&gt;
&lt;li&gt;For Next.js App Router client navigations, wrap &lt;code&gt;router.push&lt;/code&gt; in &lt;code&gt;document.startViewTransition&lt;/code&gt;, or set &lt;code&gt;experimental.viewTransition&lt;/code&gt; to use React's &lt;code&gt;&amp;lt;ViewTransition&amp;gt;&lt;/code&gt; component.&lt;/li&gt;
&lt;li&gt;React's &lt;code&gt;&amp;lt;ViewTransition&amp;gt;&lt;/code&gt; is still experimental in React 19.x — opt-in behind a flag, not stable.&lt;/li&gt;
&lt;li&gt;A duplicate &lt;code&gt;view-transition-name&lt;/code&gt; in one snapshot makes the browser skip the transition; every name must be unique per snapshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is the View Transitions API, and what problem does it solve?
&lt;/h2&gt;

&lt;p&gt;The View Transitions API is a browser API that animates the visual change between two states of the DOM. Before it, animating a route change meant keeping both the old and new UI in the DOM at once, coordinating CSS classes, and cleaning up after — a library's whole job. The API replaces that: you call &lt;code&gt;document.startViewTransition(callback)&lt;/code&gt;, the browser screenshots the current page, runs your callback to update the DOM, screenshots the new state, and cross-fades between the two snapshots. Same-document (SPA) transitions shipped in Chrome 111 in March 2023; cross-document (MPA) navigations shipped in Chrome 126 in June 2024, and Firefox and Safari have since added the same-document API.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I run my first view transition?
&lt;/h2&gt;

&lt;p&gt;Call &lt;code&gt;document.startViewTransition()&lt;/code&gt; with a callback that mutates the DOM. The default animation is a quarter-second cross-fade of the whole page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// animated&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                              &lt;span class="c1"&gt;// fallback: no animation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feature-detect first — &lt;code&gt;document.startViewTransition&lt;/code&gt; is undefined without support, and calling the update directly is the correct fallback. The method returns a &lt;code&gt;ViewTransition&lt;/code&gt; object with three promises: &lt;code&gt;.updateCallbackDone&lt;/code&gt;, &lt;code&gt;.ready&lt;/code&gt;, and &lt;code&gt;.finished&lt;/code&gt;. Await &lt;code&gt;.finished&lt;/code&gt; to run code after the transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I animate one element independently?
&lt;/h2&gt;

&lt;p&gt;Give the element a unique &lt;code&gt;view-transition-name&lt;/code&gt; and the browser animates it as its own group instead of folding it into the page cross-fade — this is the shared-element effect where a thumbnail grows into a hero image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.hero-image&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;hero&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;hero&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4s&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;During a transition the browser builds pseudo-elements: &lt;code&gt;::view-transition-old(hero)&lt;/code&gt; is the outgoing snapshot, &lt;code&gt;::view-transition-new(hero)&lt;/code&gt; the incoming one, and &lt;code&gt;::view-transition-group(hero)&lt;/code&gt; wraps both and animates position and size. The name &lt;code&gt;root&lt;/code&gt; is assigned to the whole document, so &lt;code&gt;::view-transition-old(root)&lt;/code&gt; targets the full-page cross-fade.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I add view transitions to Next.js App Router navigation?
&lt;/h2&gt;

&lt;p&gt;Wrap the navigation in &lt;code&gt;document.startViewTransition&lt;/code&gt; so the DOM update React performs during the route change becomes the transition's callback.&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="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;useRouter&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useViewTransitionRouter&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startViewTransition&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;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&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;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="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 cross-document transitions in a static or MPA-style site, skip JavaScript and opt in with CSS in both pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@view-transition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&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 community &lt;code&gt;next-view-transitions&lt;/code&gt; package wraps the client pattern if you would rather not write the hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is React's &lt;code&gt;&amp;lt;ViewTransition&amp;gt;&lt;/code&gt; component, and is it ready?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;ViewTransition&amp;gt;&lt;/code&gt; is an experimental React component that animates its children when they enter, exit, or reorder from a transition or a resolving Suspense boundary. Instead of calling &lt;code&gt;document.startViewTransition&lt;/code&gt; yourself, you wrap the changing UI and React drives the API.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unstable_ViewTransition&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ViewTransition&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ViewTransition&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;ProductList&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&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;ViewTransition&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;It is still experimental in React 19.x — the import is prefixed &lt;code&gt;unstable_&lt;/code&gt;, and in Next.js you enable it with &lt;code&gt;experimental.viewTransition: true&lt;/code&gt;. I ship the plain &lt;code&gt;document.startViewTransition&lt;/code&gt; approach in production because it is a stable browser API, and keep &lt;code&gt;&amp;lt;ViewTransition&amp;gt;&lt;/code&gt; for prototypes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks — duplicate names, reduced motion, and performance?
&lt;/h2&gt;

&lt;p&gt;The failure I hit most: two elements sharing one &lt;code&gt;view-transition-name&lt;/code&gt; in the same snapshot. The browser cannot decide which to morph, so it skips the transition and logs a warning. For a list, name only the item that is actually transitioning, using a unique id.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transition doesn't animate&lt;/td&gt;
&lt;td&gt;duplicate &lt;code&gt;view-transition-name&lt;/code&gt; in one snapshot&lt;/td&gt;
&lt;td&gt;make each name unique per snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Motion for users who opted out&lt;/td&gt;
&lt;td&gt;API always animates&lt;/td&gt;
&lt;td&gt;disable animation inside &lt;code&gt;prefers-reduced-motion: reduce&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dropped frames on low-end devices&lt;/td&gt;
&lt;td&gt;large full-screen snapshots&lt;/td&gt;
&lt;td&gt;limit named transitions to elements that must move&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;::view-transition-group&lt;/span&gt;&lt;span class="o"&gt;(*),&lt;/span&gt;
  &lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(*),&lt;/span&gt;
  &lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(*)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old and new states render as snapshots and the group moves them with transforms, which the compositor handles cheaply — but keep independently-named transitions to the elements that genuinely need to move.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Which browsers support the View Transitions API?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Same-document transitions shipped in Chrome 111 (March 2023) and are now in Firefox and Safari; cross-document transitions shipped in Chrome 126 (June 2024). Always feature-detect &lt;code&gt;document.startViewTransition&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Do I need a library for view transitions in Next.js?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. Wrapping &lt;code&gt;router.push&lt;/code&gt; in &lt;code&gt;document.startViewTransition&lt;/code&gt; covers client navigations with no dependency. &lt;code&gt;next-view-transitions&lt;/code&gt; just packages that pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Why did my view transition not animate?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Usually two elements share the same &lt;code&gt;view-transition-name&lt;/code&gt; in one snapshot, which makes the browser skip the transition. Names must be unique per captured state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Is React's &lt;code&gt;&amp;lt;ViewTransition&amp;gt;&lt;/code&gt; component stable?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. As of React 19.x it is experimental, imported as &lt;code&gt;unstable_ViewTransition&lt;/code&gt; and enabled behind &lt;code&gt;experimental.viewTransition&lt;/code&gt;. Use the browser API directly in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Can view transitions animate width and height?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes, via &lt;code&gt;::view-transition-group&lt;/code&gt;, which animates size and position between snapshots. Keep heavier animation to transform and opacity, since the old and new states are rendered as images.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/view-transitions-nextjs-app-router-field-notes" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/view-transitions-nextjs-app-router-field-notes" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>css</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Debugging Hydration Mismatches in the Next.js App Router</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Thu, 16 Jul 2026 10:18:10 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/debugging-hydration-mismatches-in-the-nextjs-app-router-1em6</link>
      <guid>https://dev.to/ahmed_mahmoud360/debugging-hydration-mismatches-in-the-nextjs-app-router-1em6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; A hydration mismatch happens when the HTML your Next.js server renders does not match what React produces on the first client render, so React 19 throws away that subtree and re-renders it in the browser — you see a flash and a console diff. The fix is almost never &lt;code&gt;suppressHydrationWarning&lt;/code&gt;; it is making the first client render deterministic.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;A hydration mismatch is when the HTML Next.js renders on the server does not match React's first client render, so React 19 discards that subtree and re-renders it in the browser.&lt;/li&gt;
&lt;li&gt;The usual causes are non-deterministic values (&lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;), browser-only APIs (&lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;) read during render, invalid HTML nesting, and DOM changes injected by browser extensions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;suppressHydrationWarning&lt;/code&gt; is not a general fix; it silences the warning for one element whose text is legitimately allowed to differ, like a timestamp.&lt;/li&gt;
&lt;li&gt;Render browser-only values with a mounted flag and &lt;code&gt;useEffect&lt;/code&gt;, or with &lt;code&gt;useSyncExternalStore&lt;/code&gt; and a server snapshot, so both sides agree.&lt;/li&gt;
&lt;li&gt;A hydration mismatch appears only on the first server-rendered load, never on client-side navigation in the App Router.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I lost the better part of an afternoon to a single flash on refresh: a timestamp column flickered and the theme toggle blinked to its default for one frame, and the console held one red line — a hydration mismatch. Once it clicked that hydration is React re-attaching to server HTML rather than re-rendering it, the whole family of bugs turned mechanical. These are my field notes from the App Router.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a hydration mismatch, and why does it break the page?
&lt;/h2&gt;

&lt;p&gt;A hydration mismatch is when the HTML Next.js renders on the server does not match the output of React's first render in the browser. During hydration, React walks the existing server DOM and attaches event listeners to it instead of recreating it, assuming the tree it computes matches the DOM already on the page. When the two differ, React 19 discards the mismatched subtree and re-renders it from scratch on the client. That re-render is the flash you see, and it forfeits the server-rendering benefit for that subtree. In development, React 19 prints a readable diff naming the element and the differing text, a large improvement over the old generic message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do &lt;code&gt;Date.now()&lt;/code&gt; and &lt;code&gt;localStorage&lt;/code&gt; cause a hydration error?
&lt;/h2&gt;

&lt;p&gt;Reading a non-deterministic or browser-only value during render is the most common cause. &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, and &lt;code&gt;new Date().toLocaleString()&lt;/code&gt; produce different output on the server than in the browser, because the server ran a moment earlier and in a different timezone and locale. Reading &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;, or &lt;code&gt;navigator&lt;/code&gt; during render is worse: the server has no such object, so it renders one branch and the client renders another.&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;// server and client compute different text -&amp;gt; mismatch&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LastSeen&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&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 server stamps the time it rendered; the browser stamps a later time in the user's locale. React sees two different strings and throws.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should I use &lt;code&gt;suppressHydrationWarning&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Reach for &lt;code&gt;suppressHydrationWarning&lt;/code&gt; only when the text of a single element is legitimately allowed to differ between server and client; a timestamp is the canonical case. It suppresses the warning for that one element, one level deep. It does not silence its children, and it is not a blanket fix.&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;time&lt;/span&gt; &lt;span class="na"&gt;suppressHydrationWarning&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;time&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;Using it to hide a real logic mismatch just trades a visible error for an invisible one. If you find yourself adding it to more than a couple of leaf nodes, the real problem is elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I render a client-only value without a mismatch?
&lt;/h2&gt;

&lt;p&gt;Render a stable placeholder on the server and swap in the real value after mount. For a theme, a viewport width, or a &lt;code&gt;localStorage&lt;/code&gt; preference, the two-pass pattern uses a mounted flag:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Theme&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;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMounted&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;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="nf"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;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;mounted&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="nc"&gt;ThemeSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt; &lt;span class="c1"&gt;// same markup on server + first client render&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;ThemeToggle&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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;Both the server and the first client render return the skeleton, so they match; the real value appears on the second render. For external browser state, &lt;code&gt;useSyncExternalStore&lt;/code&gt; is cleaner because its third argument is a server snapshot:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isWide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(min-width: 1024px)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// client&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;                                            &lt;span class="c1"&gt;// server snapshot&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supplying that server value is what keeps hydration consistent. For a component that can never render on the server, &lt;code&gt;next/dynamic&lt;/code&gt; with &lt;code&gt;ssr: false&lt;/code&gt; skips server rendering entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do browser extensions cause hydration errors on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Browser extensions cause hydration errors by mutating the DOM before React hydrates. Grammarly injects &lt;code&gt;data-gramm&lt;/code&gt; attributes, password managers add wrappers, and dark-mode extensions rewrite inline styles on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. The server sent clean markup; by the time React hydrates, the DOM carries extra attributes, so they mismatch. Because you cannot control the user's extensions, the accepted fix is &lt;code&gt;suppressHydrationWarning&lt;/code&gt; on the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; elements in your root layout, which the Next.js documentation recommends. It is safe there because those elements carry no dynamic text of your own to protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I fix invalid HTML nesting mismatches?
&lt;/h2&gt;

&lt;p&gt;Invalid HTML nesting causes mismatches that look mysterious because your JSX seems fine. The browser silently repairs illegal nesting by moving or closing tags, so the DOM it built no longer matches the tree React expects, and React reports a mismatch on an element you never touched.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Invalid nesting&lt;/th&gt;
&lt;th&gt;Why the browser rewrites it&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; inside &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; auto-closes before a block element&lt;/td&gt;
&lt;td&gt;use a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; inside &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a nested &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; is not allowed, so the outer closes early&lt;/td&gt;
&lt;td&gt;flatten to a single &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; inside &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;interactive elements cannot nest&lt;/td&gt;
&lt;td&gt;restructure so links are siblings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; without &lt;code&gt;&amp;lt;tbody&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the browser inserts a &lt;code&gt;&amp;lt;tbody&amp;gt;&lt;/code&gt; for you&lt;/td&gt;
&lt;td&gt;add the &lt;code&gt;&amp;lt;tbody&amp;gt;&lt;/code&gt; yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fix is always to make the JSX produce valid HTML. React 19's diff usually points at the reparented node, which is the fastest way to find it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the difference between a hydration mismatch and a rendering error?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; A hydration mismatch is specifically the server HTML disagreeing with the first client render, and it appears only on initial load, never on client-side navigation. A rendering error is any exception thrown while rendering, at any time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Does &lt;code&gt;suppressHydrationWarning&lt;/code&gt; fix the flash?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. It silences the console warning for one element, but React still re-renders the mismatched content on the client. Use it only when the difference is intentional, like a timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Why does the error only happen on refresh and not when I navigate?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Hydration runs once, on the first server-rendered load. Client-side navigation in the App Router renders entirely in the browser, so there is no server HTML to mismatch against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Can &lt;code&gt;useEffect&lt;/code&gt; cause a hydration mismatch?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. &lt;code&gt;useEffect&lt;/code&gt; runs after hydration, so state you set inside it cannot affect the first render React compares against the server HTML. That is why the mounted-flag pattern is safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; How do I find which component is mismatching?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Read the React 19 development console diff, which names the element and shows the differing text. If you suspect an extension, reproduce in an incognito window with extensions disabled.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/debugging-hydration-mismatches-nextjs-app-router" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/debugging-hydration-mismatches-nextjs-app-router" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Fixing INP: Field Notes on Interaction to Next Paint in a React App</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Thu, 16 Jul 2026 06:04:51 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/fixing-inp-field-notes-on-interaction-to-next-paint-in-a-react-app-32fa</link>
      <guid>https://dev.to/ahmed_mahmoud360/fixing-inp-field-notes-on-interaction-to-next-paint-in-a-react-app-32fa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; Interaction to Next Paint (INP) is the Core Web Vitals metric that measures how long your page takes to visually respond to a user interaction, and since March 2024 it replaced First Input Delay (FID). When my dashboard felt sluggish on every click, the fix was not a faster server — it was getting React off the main thread across the three phases of each interaction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I spent the better part of a week chasing a "the app feels slow" complaint that no server metric explained. The API was fast, Lighthouse scored green in the lab, but every click on the filter panel lagged before anything changed on screen. The culprit was INP, and splitting each interaction into three phases made the fixes small and mechanical.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;INP (Interaction to Next Paint)&lt;/strong&gt; measures the latency from a user interaction to the next frame the browser paints; 200 ms or less is good, over 500 ms is poor, both at the 75th percentile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP replaced First Input Delay (FID)&lt;/strong&gt; as a Core Web Vital in March 2024 — FID measured only the first interaction's input delay; INP measures the full response of nearly every interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every interaction has three phases:&lt;/strong&gt; input delay, processing time, presentation delay. Fix the one that dominates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two highest-leverage React fixes&lt;/strong&gt; are &lt;code&gt;startTransition&lt;/code&gt; for non-urgent updates and &lt;code&gt;scheduler.yield()&lt;/code&gt; for long tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP is a field metric&lt;/strong&gt; — measure it with the &lt;code&gt;web-vitals&lt;/code&gt; library, not Lighthouse.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is INP and how is it different from FID?
&lt;/h2&gt;

&lt;p&gt;INP reports the longest interaction latency a user experiences on a page, from click, tap, or keypress until the next painted frame. Google promoted INP to a core metric in March 2024, retiring FID. FID measured only the input delay of the first interaction and ignored everything after the handler started. INP measures the whole interaction — input delay plus handler work plus rendering — and reports the worst one across the session. A page can post a perfect FID and still feel broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the three phases of an interaction?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input delay:&lt;/strong&gt; action until the event handler starts, usually waiting on a busy main thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing time:&lt;/strong&gt; how long your handlers run, including React state updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation delay:&lt;/strong&gt; end of processing until the next paint, dominated by layout and paint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Record an interaction in the Chrome DevTools Performance panel and it labels all three. Optimize the one that is actually long.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I fix input delay?
&lt;/h2&gt;

&lt;p&gt;Input delay is almost always a long task — any JavaScript block occupying the main thread for over 50 ms — blocking the click. Break long tasks into chunks and yield between them with &lt;code&gt;scheduler.yield()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;chunkOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;renderChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&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;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// hand the main thread back so a queued click runs&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It resumes after the browser handles higher-priority work like input. My biggest win was deferring a third-party analytics init with &lt;code&gt;requestIdleCallback&lt;/code&gt; so it stopped blocking first clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I fix processing time?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;startTransition&lt;/code&gt; marks a React state update as non-urgent so React can interrupt it and keep the interaction responsive:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                      &lt;span class="c1"&gt;// urgent: keep the input live&lt;/span&gt;
  &lt;span class="nf"&gt;startTransition&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;setResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// non-urgent: interruptible&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;useDeferredValue&lt;/code&gt; does the same for a value you receive. Neither is faster — both are interruptible, which is what INP measures. For heavy computation, move it to a Web Worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I fix presentation delay?
&lt;/h2&gt;

&lt;p&gt;Presentation delay is rendering work on too much DOM. &lt;code&gt;content-visibility: auto&lt;/code&gt; skips off-screen elements until they scroll near the viewport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.list-row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;content-visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;contain-intrinsic-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt; &lt;span class="m"&gt;48px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* reserve height so the scrollbar stays stable */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other cause is layout thrashing — reading &lt;code&gt;offsetHeight&lt;/code&gt; then writing a style in the same loop. Batch reads, then writes. For long lists, virtualization beats any CSS trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I measure INP in the field?
&lt;/h2&gt;

&lt;p&gt;INP is a field metric, so Lighthouse's lab number is only an estimate — it cannot click your buttons. Capture the real one with &lt;code&gt;web-vitals&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onINP&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;web-vitals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;onINP&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;metric&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="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/vitals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&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 attribution build names the slow element and phase, and Chrome's CrUX field data confirms whether the 75th-percentile score actually moved.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is a good INP score?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; 200 ms or less at the 75th percentile is good, 200–500 ms needs improvement, over 500 ms is poor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Is INP a Core Web Vital?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes — it replaced First Input Delay in March 2024.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Does &lt;code&gt;startTransition&lt;/code&gt; make my code faster?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. It marks an update non-urgent so React can interrupt it; the work takes the same time but stops blocking the interaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Can I measure INP in Lighthouse?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Not reliably. Use the &lt;code&gt;web-vitals&lt;/code&gt; library or Chrome's CrUX field data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is &lt;code&gt;scheduler.yield()&lt;/code&gt;?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; A browser API that pauses a long task and resumes it after higher-priority work like input, keeping input delay low.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/inp-interaction-to-next-paint-react-field-notes" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/inp-interaction-to-next-paint-react-field-notes" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>performance</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Next.js Server Actions Are Public Endpoints: How I Lock Them Down</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Mon, 13 Jul 2026 03:38:53 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/nextjs-server-actions-are-public-endpoints-how-i-lock-them-down-34fa</link>
      <guid>https://dev.to/ahmed_mahmoud360/nextjs-server-actions-are-public-endpoints-how-i-lock-them-down-34fa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; Every Next.js Server Action is a public HTTP endpoint. &lt;code&gt;"use server"&lt;/code&gt; marks a function to be called over the network—not protected from the network.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;I shipped a feature last quarter that let users export their data. The export logic lived in a Server Action. Three weeks later I realized anyone with a browser console could call that action directly—no session, no auth check—because I forgot to verify the session inside the action itself.&lt;/p&gt;

&lt;p&gt;This post is what I learned hardening Server Actions at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Server Actions Are Different
&lt;/h2&gt;

&lt;p&gt;Traditional APIs have a clear boundary: your route handlers live in &lt;code&gt;app/api/&lt;/code&gt; and you consciously design them as public endpoints. Server Actions blur that line. You write them inline, they feel like private functions, but Next.js exposes each one as a &lt;code&gt;POST&lt;/code&gt; endpoint at a URL derived from a build-time hash.&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="c"&gt;# What actually happens under the hood&lt;/span&gt;
POST /_next/action/abc123def456
Content-Type: application/x-www-form-urlencoded

&lt;span class="o"&gt;[&lt;/span&gt;encrypted action arguments]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The arguments are encrypted, but the &lt;em&gt;endpoint exists&lt;/em&gt;. If your action doesn't check auth, an attacker who finds the action ID can call it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mistakes I See Most
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Assuming &lt;code&gt;"use server"&lt;/code&gt; Means Private
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ This is a public HTTP endpoint&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&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;exportUserData&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// No auth check — anyone can call this&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM users WHERE id = $1&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;userId&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 action is exposed. The arguments aren't, but the endpoint is.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Missing Session Checks Inside Actions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Check session inside the action itself&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&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;auth&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;@/lib/auth&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;exportUserData&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="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;auth&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="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="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;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&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="o"&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;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM users WHERE id = $1&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;userId&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;h3&gt;
  
  
  3. Trusting Client-Provided Values Without Validation
&lt;/h3&gt;

&lt;p&gt;Server Actions receive arguments from the client. Even with encryption, you control what your client sends. Validate everything:&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;// ✅ Validate inputs with Zod&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;z&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;zod&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;ExportSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;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;exportUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;auth&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="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;Unauthorized&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="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="nx"&gt;format&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ExportSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;userId&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="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="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;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generateExport&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="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Pattern I Now Use Everywhere
&lt;/h2&gt;

&lt;p&gt;I've settled on a wrapper that enforces auth before any action runs:&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;// lib/safe-action.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;auth&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;@/lib/auth&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;Session&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-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AuthenticatedAction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&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;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="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;requireAuth&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthenticatedAction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="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;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;auth&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="nx"&gt;user&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;Authentication required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;action&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;input&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="c1"&gt;// Usage&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;exportUserData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="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="nx"&gt;format&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ExportSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;userId&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="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="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;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generateExport&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="nx"&gt;format&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 makes the auth requirement impossible to forget—you can't write the action without thinking about the session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limiting and Abuse Prevention
&lt;/h2&gt;

&lt;p&gt;Because actions are HTTP endpoints, they're vulnerable to abuse. Add rate limiting at the middleware level:&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;NextRequest&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ratelimit&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;@upstash/ratelimit&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;Redis&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;@upstash/redis&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;ratelimit&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;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnv&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slidingWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;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;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_next/action/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;success&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;ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&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;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Too many requests&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Recommend Auditing Now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search your codebase&lt;/strong&gt; for &lt;code&gt;"use server"&lt;/code&gt; files. List every exported function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check each one&lt;/strong&gt; for: session verification, input validation, authorization (not just authentication).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the &lt;code&gt;requireAuth&lt;/code&gt; wrapper&lt;/strong&gt; (or equivalent) to every action that touches user data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add rate limiting&lt;/strong&gt; at middleware for action endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log action calls&lt;/strong&gt; with user ID and input shape—not full values—so you have an audit trail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Server Actions are a great DX improvement. They're also a new attack surface if you treat them as private functions when they're actually public endpoints.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/nextjs-server-actions-security-field-notes" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also at &lt;a href="https://eng-ahmed.com/blog/nextjs-server-actions-security-field-notes" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>security</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React Compiler in 2026: What It Actually Memoizes (And What It Doesn't)</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Sun, 12 Jul 2026 00:18:56 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/react-compiler-in-2026-what-it-actually-memoizes-and-what-it-doesnt-5ga1</link>
      <guid>https://dev.to/ahmed_mahmoud360/react-compiler-in-2026-what-it-actually-memoizes-and-what-it-doesnt-5ga1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; React Compiler â€” formerly React Forget â€” shipped stable with React 19 and automatically memoizes components, hooks, and callbacks by analyzing data flow at build time. No dependency arrays to write; the compiler infers them. Here is what it handles, when it opts out, and whether you should delete your &lt;code&gt;useMemo&lt;/code&gt; calls.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;React Compiler inserts &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, and &lt;code&gt;React.memo&lt;/code&gt; automatically at build time â€” no dependency arrays to maintain.&lt;/li&gt;
&lt;li&gt;Enable it in Next.js 15/16 with &lt;code&gt;experimental.reactCompiler: true&lt;/code&gt; in &lt;code&gt;next.config.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The compiler is conservative: if it cannot prove memoization is safe, it emits the component unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"use no memo"&lt;/code&gt; is the escape hatch for functions the compiler should not touch.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npx react-compiler-healthcheck@latest&lt;/code&gt; before enabling to see coverage and violations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does React Compiler actually do?
&lt;/h2&gt;

&lt;p&gt;React Compiler transforms component and hook code at build time to insert memoization automatically. Instead of &lt;code&gt;useMemo(() =&amp;gt; expensiveCalc(a, b), [a, b])&lt;/code&gt;, the compiler analyzes data flow, determines which values are stable across renders, and emits equivalent memoized code. The compiled output uses React's memo infrastructure at runtime. The compiler is &lt;code&gt;babel-plugin-react-compiler&lt;/code&gt; â€” it works with any Babel-based build pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I enable it in Next.js?
&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="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;reactCompiler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="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;Before enabling, run the healthcheck:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx react-compiler-healthcheck@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The healthcheck reports optimizable component count, files with violations, and blocking patterns. Fix violations first for more coverage on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the compiler memoize?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Components&lt;/strong&gt; â€” equivalent to &lt;code&gt;React.memo&lt;/code&gt;; re-renders only when props change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Values&lt;/strong&gt; â€” equivalent to &lt;code&gt;useMemo&lt;/code&gt;; computed results, derived arrays, objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Callbacks&lt;/strong&gt; â€” equivalent to &lt;code&gt;useCallback&lt;/code&gt;: event handlers, functions passed as props.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dependencies are inferred from escape analysis â€” no dependency array needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does it opt out?
&lt;/h2&gt;

&lt;p&gt;The compiler skips a function and emits it unchanged when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Props/state mutated directly&lt;/strong&gt; â€” &lt;code&gt;props.items.push(x)&lt;/code&gt; violates React rules; compiler detects and opts out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutable external references read&lt;/strong&gt; â€” a singleton the compiler cannot prove is stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules of Hooks violated&lt;/strong&gt; â€” hooks called conditionally cause the whole file to be skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex &lt;code&gt;let&lt;/code&gt; reassignment&lt;/strong&gt; â€” non-obvious variable flows the compiler cannot track.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;"use no memo"&lt;/code&gt; escape hatch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MyComponent&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="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="s2"&gt;use no memo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="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;Use when compiler output is wrong for a specific function or you are wrapping a library relying on reference instability. Intentionally verbose â€” should feel deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should I delete useMemo and useCallback?
&lt;/h2&gt;

&lt;p&gt;For components the compiler handles: yes, eventually. The healthcheck shows which are optimized â€” those are safe to clean up. Keep manual memoization for non-optimizable components, &lt;code&gt;useMemo&lt;/code&gt; as a semantic signal for expensive computations, and third-party components you cannot change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual vs compiler memoization
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Manual hooks&lt;/th&gt;
&lt;th&gt;React Compiler&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dependency arrays&lt;/td&gt;
&lt;td&gt;You write them&lt;/td&gt;
&lt;td&gt;Compiler infers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale closure risk&lt;/td&gt;
&lt;td&gt;Real â€” wrong deps cause bugs&lt;/td&gt;
&lt;td&gt;Eliminated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;Only annotated values&lt;/td&gt;
&lt;td&gt;All optimizable code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escape hatch&lt;/td&gt;
&lt;td&gt;Remove the hook call&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"use no memo"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;strong&gt;Q: Does it work without Next.js?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes â€” &lt;code&gt;babel-plugin-react-compiler&lt;/code&gt; works with Vite, webpack, or any Babel pipeline. Next.js's &lt;code&gt;experimental.reactCompiler&lt;/code&gt; is a convenience wrapper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Will it break existing components?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No â€” it only memoizes what it can prove is correct and leaves everything else unchanged. Run the healthcheck first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Does it replace React.memo?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; For fully analyzable components, yes. &lt;code&gt;React.memo&lt;/code&gt; on compiler-skipped components still adds value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I enable gradually?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes â€” pass an &lt;code&gt;includes&lt;/code&gt; filter to &lt;code&gt;babel-plugin-react-compiler&lt;/code&gt; to target specific directories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What React version is required?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; React 19+. Does not work on React 18.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/react-compiler-field-notes-2026" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/react-compiler-field-notes-2026" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Partial Prerendering in Next.js: The Static Shell + Dynamic Stream Model</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Sun, 12 Jul 2026 00:16:41 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/partial-prerendering-in-nextjs-the-static-shell-dynamic-stream-model-16hh</link>
      <guid>https://dev.to/ahmed_mahmoud360/partial-prerendering-in-nextjs-the-static-shell-dynamic-stream-model-16hh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; Partial Prerendering (PPR) in Next.js serves a static HTML shell from the CDN edge instantly, then streams Suspense-wrapped dynamic children from the origin in the same HTTP response. No full-page ISR staleness, no full-page origin latency. I shipped it on two production routes â€” here is the model.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PPR serves a static HTML shell from the CDN edge&lt;/strong&gt;, then streams dynamic Suspense children from the origin in the same response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The static shell is built at build time&lt;/strong&gt; â€” outside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; renders statically; inside renders dynamically per request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PPR replaces the ISR vs. dynamic tradeoff&lt;/strong&gt; for pages that are mostly static with isolated personalized sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No changes to Server Components or Suspense&lt;/strong&gt; â€” just &lt;code&gt;experimental.ppr: 'incremental'&lt;/code&gt; in config and &lt;code&gt;export const experimental_ppr = true&lt;/code&gt; per route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PPR and &lt;code&gt;use cache&lt;/code&gt; are complementary&lt;/strong&gt;: CDN delivery for the shell, origin memoization for dynamic islands.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does PPR actually do?
&lt;/h2&gt;

&lt;p&gt;PPR splits a page into two rendering phases within the same HTTP response. At build time, Next.js freezes everything that does not read dynamic request data into a static HTML shell on the CDN edge. At request time, the CDN delivers the shell at edge latency while the origin streams each &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary's content into the same response.&lt;/p&gt;

&lt;p&gt;On a product page: navigation, title, and description arrive at CDN speed. The in-stock badge and personalized recommendations stream from the origin a fraction of a second later. The user sees a nearly-complete page immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  How is PPR different from ISR and streaming Suspense?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;First byte&lt;/th&gt;
&lt;th&gt;Dynamic freshness&lt;/th&gt;
&lt;th&gt;Staleness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ISR (revalidate: N)&lt;/td&gt;
&lt;td&gt;CDN edge&lt;/td&gt;
&lt;td&gt;Whole page up to N seconds stale&lt;/td&gt;
&lt;td&gt;Full page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic rendering&lt;/td&gt;
&lt;td&gt;Origin&lt;/td&gt;
&lt;td&gt;100% fresh; waits for slowest query&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming Suspense (no PPR)&lt;/td&gt;
&lt;td&gt;Origin&lt;/td&gt;
&lt;td&gt;Fresh; TTFB includes origin latency&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PPR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CDN edge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Dynamic islands 100% fresh&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Static shell only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How do I enable PPR?
&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ppr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;incremental&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;// app/products/[slug]/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;experimental_ppr&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="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;ProductPage&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="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&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="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;main&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;ProductHeader&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* static shell â€” CDN */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StockSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StockAvailability&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* dynamic â€” origin */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&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;Anything outside &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; must not read dynamic data. If it calls &lt;code&gt;cookies()&lt;/code&gt;, the build fails with a clear error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs in the shell vs. inside Suspense?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Static shell:&lt;/strong&gt; navigation, footer, static content, layout structure, images independent of the user's session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside Suspense:&lt;/strong&gt; user-specific content (cart, recommendations), real-time inventory or pricing, A/B cookie reads, any component calling &lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;, or &lt;code&gt;searchParams&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do PPR and use cache compose?
&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 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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductDescription&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;slug&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="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;minutes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product-desc-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;desc&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&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;desc&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;description&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PPR controls the CDN boundary. &lt;code&gt;use cache&lt;/code&gt; memoizes origin renders inside Suspense. Both stack: CDN for the shell, component cache for the dynamic parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should I use PPR vs. full dynamic rendering?
&lt;/h2&gt;

&lt;p&gt;Use PPR when the page is mostly static with isolated dynamic sections â€” a product page with real-time inventory, a SaaS dashboard with a cached sidebar plus live metrics. If the entire page depends on per-request data, full dynamic rendering is simpler.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q: Does PPR require changes to Server Components or Suspense?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. Only the config flag and per-route export. All existing patterns work unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What happens to SEO with PPR?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; The static shell is fully crawlable HTML from the CDN. Suspense-streamed content may or may not be indexed by crawlers. Keep title, description, and primary body text in the static shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I use PPR and ISR on the same route?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. A route opts into either PPR or ISR. PPR replaces ISR: static shell is the build-time snapshot, dynamic Suspense children are always fresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is PPR stable in Next.js 15 and 16?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Incremental opt-in, experimental through Next.js 16. Production-safe (Vercel uses it internally) but API may change before graduating to stable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I verify PPR is working?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Run &lt;code&gt;next build&lt;/code&gt;. Routes with PPR show a split between static and dynamic segments in terminal output. In Chrome DevTools Network, the response arrives in chunks: static shell first, then the Suspense stream.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/partial-prerendering-nextjs-ppr-field-notes" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/partial-prerendering-nextjs-ppr-field-notes" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>I Replaced ESLint and Prettier with Biome on Two Production Repos</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Sun, 12 Jul 2026 00:12:27 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/i-replaced-eslint-and-prettier-with-biome-on-two-production-repos-2m2a</link>
      <guid>https://dev.to/ahmed_mahmoud360/i-replaced-eslint-and-prettier-with-biome-on-two-production-repos-2m2a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; Biome is a single Rust-based toolchain that formats and lints JavaScript and TypeScript in one pass, replacing the ESLint + Prettier pair. On two production repos it collapsed two configs, two dependency trees, and two editor plugins into one binary â€” and turned the lint-and-format step from several seconds into well under a second. It does not yet cover every ESLint plugin, so here is where it won and where I kept ESLint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For years my default JS setup was ESLint for correctness, Prettier for formatting, &lt;code&gt;eslint-config-prettier&lt;/code&gt; to keep them from arguing, and an import-sorting plugin. It works, but it is four dependencies, two configs, two editor extensions, and a pre-commit hook running two tools. Biome â€” one Rust binary that does both â€” has matured enough that I migrated two production repos to it. These are the field notes.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Biome replaces both ESLint and Prettier&lt;/strong&gt; with one binary, one config (&lt;code&gt;biome.json&lt;/code&gt;), and one command, &lt;code&gt;biome check --write&lt;/code&gt;, that formats, lints, and organizes imports in a single pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The migration is mostly automated:&lt;/strong&gt; &lt;code&gt;biome migrate eslint --write&lt;/code&gt; and &lt;code&gt;biome migrate prettier --write&lt;/code&gt; translate your configs, but plugin-specific rules do not carry over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Biome 2.0 added type-aware lint rules that run without the TypeScript compiler&lt;/strong&gt;, so &lt;code&gt;noFloatingPromises&lt;/code&gt; works without wiring up &lt;code&gt;tsc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed is the headline win:&lt;/strong&gt; one Rust binary runs format, lint, and import sorting in parallel, finishing a large repo in a fraction of the time ESLint + Prettier takes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a complete ESLint replacement yet:&lt;/strong&gt; the plugin ecosystem is young, so you may run Biome and ESLint side by side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What does Biome actually replace?
&lt;/h2&gt;

&lt;p&gt;Biome is a single toolchain combining a formatter (a near-drop-in Prettier replacement) and a linter (an ESLint-style rule engine) in one Rust binary. It stands in for four parts at once: Prettier, ESLint, the &lt;code&gt;eslint-config-prettier&lt;/code&gt; glue, and a separate import-sorting plugin. One &lt;code&gt;biome.json&lt;/code&gt; holds formatter options, lint rules, and import organization, and one command does the whole job:&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="c"&gt;# format, lint, and organize imports in a single pass&lt;/span&gt;
biome check &lt;span class="nt"&gt;--write&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When should I switch from ESLint and Prettier to Biome?
&lt;/h2&gt;

&lt;p&gt;Switch when your linting stack has become its own maintenance burden and you are not relying on niche ESLint plugins. The signals that pushed me: lint + format slow enough to notice in CI or pre-commit; an ESLint config that has drifted into flat-config plus plugins nobody fully understands; mostly standard rules rather than custom ones; and wanting type-aware rules without the full type-check cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I migrate an existing ESLint + Prettier setup?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;--save-exact&lt;/span&gt; @biomejs/biome
npx biome migrate eslint &lt;span class="nt"&gt;--write&lt;/span&gt;
npx biome migrate prettier &lt;span class="nt"&gt;--write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;biome migrate eslint&lt;/code&gt; maps ESLint rules with Biome equivalents into &lt;code&gt;biome.json&lt;/code&gt;; &lt;code&gt;biome migrate prettier&lt;/code&gt; copies your Prettier options so formatting stays visually identical. Rules from plugins without a Biome counterpart are dropped, so diff the generated config against your old rule list and make a call on each gap. Then delete ESLint, Prettier, and &lt;code&gt;eslint-config-prettier&lt;/code&gt;, and re-point the pre-commit hook and editor at Biome.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does Biome 2.0's type-aware linting change?
&lt;/h2&gt;

&lt;p&gt;Biome 2.0 introduced type-aware lint rules that run without invoking the TypeScript compiler. ESLint rules that reason about types need &lt;code&gt;typescript-eslint&lt;/code&gt; with type information, which runs &lt;code&gt;tsc&lt;/code&gt; as part of linting â€” usually the slowest part. Biome does its own lightweight type inference in Rust, so &lt;code&gt;noFloatingPromises&lt;/code&gt; works without a type-check pass. It will not replace &lt;code&gt;tsc&lt;/code&gt; for build-time type safety, but for the type-aware rules it covers it is far cheaper to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does Biome work in a monorepo?
&lt;/h2&gt;

&lt;p&gt;Biome 2.0 supports nested config: a root &lt;code&gt;biome.json&lt;/code&gt; defines shared defaults, and each package extends it and overrides locally with &lt;code&gt;"extends": "//"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;packages/api/biome.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"//"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"linter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"suspicious"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"noConsole"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Biome vs ESLint + Prettier at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;ESLint + Prettier&lt;/th&gt;
&lt;th&gt;Biome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Moving parts&lt;/td&gt;
&lt;td&gt;Two tools, two configs, a glue package&lt;/td&gt;
&lt;td&gt;One binary, one &lt;code&gt;biome.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed on a large repo&lt;/td&gt;
&lt;td&gt;Several seconds&lt;/td&gt;
&lt;td&gt;Typically sub-second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import sorting&lt;/td&gt;
&lt;td&gt;Separate plugin&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type-aware rules&lt;/td&gt;
&lt;td&gt;Need typescript-eslint + tsc&lt;/td&gt;
&lt;td&gt;Built-in inference, no tsc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin ecosystem&lt;/td&gt;
&lt;td&gt;Thousands of mature plugins&lt;/td&gt;
&lt;td&gt;Young (GritQL plugins)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formatting&lt;/td&gt;
&lt;td&gt;Prettier&lt;/td&gt;
&lt;td&gt;Near-Prettier parity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What still keeps me on ESLint?
&lt;/h2&gt;

&lt;p&gt;Biome's rule set does not yet match every rule from plugins like &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; or &lt;code&gt;eslint-plugin-import&lt;/code&gt;, and its GritQL plugin story is far younger than ESLint's ecosystem. On one repo with strict accessibility rules and a couple of custom org-specific rules, I kept a slim ESLint config for exactly those and let Biome own formatting, import sorting, and the bulk of correctness linting. Scoping ESLint down to the few rules Biome cannot express still removed most of the old overhead.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Is Biome a drop-in replacement for Prettier?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; For formatting it is near-drop-in â€” Biome targets high Prettier compatibility, and you will see only a handful of edge-case differences after migrating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Does Biome need the TypeScript compiler for type-aware rules?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. Since Biome 2.0, rules like &lt;code&gt;noFloatingPromises&lt;/code&gt; run on Biome's own inference without &lt;code&gt;tsc&lt;/code&gt;. Biome does not replace &lt;code&gt;tsc&lt;/code&gt; for full type checking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Can I run Biome and ESLint at the same time?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes â€” a common transitional setup. Let Biome handle formatting, imports, and standard rules, and keep a scoped ESLint config only for plugin rules Biome has no equivalent for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Which languages does Biome support?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; JavaScript, TypeScript, JSX/TSX, JSON, and CSS, with GraphQL support and more progressing. Markdown and YAML formatting are still maturing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; How do I migrate without hand-writing a config?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Install &lt;code&gt;@biomejs/biome&lt;/code&gt;, then run &lt;code&gt;biome migrate eslint --write&lt;/code&gt; and &lt;code&gt;biome migrate prettier --write&lt;/code&gt;. Review the generated &lt;code&gt;biome.json&lt;/code&gt;, since rules from unsupported plugins are dropped.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/biome-vs-eslint-prettier-field-notes-2026" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/biome-vs-eslint-prettier-field-notes-2026" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>biome</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>TanStack Query in 2026: What Server Components Didn't Replace</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Sun, 12 Jul 2026 00:09:00 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/tanstack-query-in-2026-what-server-components-didnt-replace-5d49</link>
      <guid>https://dev.to/ahmed_mahmoud360/tanstack-query-in-2026-what-server-components-didnt-replace-5d49</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; When React Server Components and Server Actions landed, the loudest take was "you don't need a client data-fetching library anymore." After two years of shipping App Router apps, that's half true. RSC replaced the default fetch, not the whole job â€” here's where I still reach for TanStack Query, and where I've deleted it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What RSC and Server Actions actually replaced
&lt;/h2&gt;

&lt;p&gt;For a page's initial data, a server component that awaits your database is simpler than anything a client library gives you: no loading state, no client-side waterfall, no serialization boilerplate, and the data never becomes JSON the browser re-parses.&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/dashboard/page.tsx â€” no client library involved&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;Dashboard&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;projects&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&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="nc"&gt;ProjectList&lt;/span&gt; &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;projects&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;For simple mutations, a Server Action plus &lt;code&gt;revalidatePath&lt;/code&gt; covers create/update/delete without a mutation hook or a query key. If your app is mostly forms and page-level reads, ship it without a client cache â€” it'll be smaller and easier to reason about. I deleted TanStack Query from two projects that fit this shape and didn't miss it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a client cache still earns its place
&lt;/h2&gt;

&lt;p&gt;Every case where I kept TanStack Query shares one trait: the data changes on the client's timeline, not the request's. Server components render once per request. The moment the UI needs to poll, refetch, paginate, or share cached data across routes without a full navigation, you're back in client-cache territory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polling / near-real-time.&lt;/strong&gt; A build-status widget needs &lt;code&gt;refetchInterval&lt;/code&gt;. There's no server-component equivalent to "re-run every few seconds without navigating."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infinite scroll.&lt;/strong&gt; &lt;code&gt;useInfiniteQuery&lt;/code&gt; manages cursors, dedupes in-flight requests, and keeps prior pages mounted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-route cache sharing.&lt;/strong&gt; When several routes read the same current user or org settings, a shared cache serves them instantly on client navigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic updates across components.&lt;/strong&gt; &lt;code&gt;useOptimistic&lt;/code&gt; is great for one form; when a mutation should optimistically touch a list, a counter, and a header badge at once, a shared cache with &lt;code&gt;setQueryData&lt;/code&gt; is cleaner.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern that makes them coexist: hydration
&lt;/h2&gt;

&lt;p&gt;The mistake I made early was treating it as either/or. The best setup uses both â€” fetch on the server, hand the result to TanStack Query so the client cache starts warm and the first render has zero loading state.&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/projects/page.tsx (Server Component)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qc&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;QueryClient&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;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prefetchQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&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;projects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&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;HydrationBoundary&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;dehydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProjectsClient&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;HydrationBoundary&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 client reads the same query key, renders instantly from the hydrated cache, and owns freshness from there â€” polling, refetch-on-focus, and invalidation all work as usual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutations: revalidatePath vs invalidateQueries
&lt;/h2&gt;

&lt;p&gt;This is where I see the most confusion. Both invalidate caches â€” different caches. &lt;code&gt;revalidatePath&lt;/code&gt; re-renders server components for a route; &lt;code&gt;invalidateQueries&lt;/code&gt; refetches a client query. If your data lives in the client cache, a Server Action that only calls &lt;code&gt;revalidatePath&lt;/code&gt; won't touch it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mutate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NewProject&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;createProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// server action&lt;/span&gt;
  &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&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;projects&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My rule: one owner per piece of data. If a value is read through &lt;code&gt;useQuery&lt;/code&gt;, the mutation that changes it must invalidate that query â€” don't rely on &lt;code&gt;revalidatePath&lt;/code&gt; to keep the client cache honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  My current decision rule
&lt;/h2&gt;

&lt;p&gt;Start without it. Render reads in server components, write through Server Actions, and add TanStack Query only when a concrete need shows up: polling, infinite scroll, a shared client cache across routes, or multi-component optimistic updates. Adding it later is a small, local change; ripping out a cache you didn't need is not. RSC and a client cache answer different questions â€” one owns the initial server render, the other owns client-timeline freshness â€” and the apps I'm happiest with use each for exactly what it's good at.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/tanstack-query-2026-what-server-components-didnt-replace" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/tanstack-query-2026-what-server-components-didnt-replace" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>useOptimistic in React 19: Patterns and Pitfalls from Real Forms</title>
      <dc:creator>Ahmed Mahmoud</dc:creator>
      <pubDate>Sun, 12 Jul 2026 00:05:37 +0000</pubDate>
      <link>https://dev.to/ahmed_mahmoud360/useoptimistic-in-react-19-patterns-and-pitfalls-from-real-forms-41em</link>
      <guid>https://dev.to/ahmed_mahmoud360/useoptimistic-in-react-19-patterns-and-pitfalls-from-real-forms-41em</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; React 19's &lt;code&gt;useOptimistic&lt;/code&gt; shows the result of an action before the server confirms it. The API is tiny; the mental model is where people trip. Here's what held up across a comment box, a like button, and a settings form â€” plus three edges that cost me an afternoon each.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useOptimistic&lt;/code&gt; stores nothing durable. It's a view over your real state that is only live while a transition is pending.&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;optimistic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addOptimistic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useOptimistic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;realState&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;action&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;nextState&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first arg is the source of truth; the second is a pure reducer. The instant your action resolves and &lt;code&gt;realState&lt;/code&gt; updates, React throws the optimistic value away and re-renders from truth. You never manually roll back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: append on submit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;optimisticComments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addOptimistic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useOptimistic&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pending&lt;/code&gt; flag drives a dimmed style; the server action revalidates the list so it refreshes from source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 1: fire it inside a transition
&lt;/h2&gt;

&lt;p&gt;Calling &lt;code&gt;addOptimistic&lt;/code&gt; from a plain &lt;code&gt;onClick&lt;/code&gt; makes the value flash and vanish â€” there's no pending transition to keep it alive. Always trigger it from a form action or &lt;code&gt;startTransition&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: toggle with a count
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useOptimistic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;liked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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="na"&gt;liked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;liked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;liked&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;Because the reducer receives the current optimistic value, rapid double-clicks compose correctly instead of fighting a stale closure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 2: errors need their own signal
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useOptimistic&lt;/code&gt; has no error channel. When the action throws, the optimistic value disappears â€” correct â€” but the user gets no explanation. Pair it with &lt;code&gt;useActionState&lt;/code&gt; or a toast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 3: unique temporary keys
&lt;/h2&gt;

&lt;p&gt;Reusing &lt;code&gt;id: 'temp'&lt;/code&gt; for every pending row makes React merge concurrent entries. Give each optimistic item a unique temp key and let the server row replace it on revalidation.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I skip it
&lt;/h2&gt;

&lt;p&gt;For low-confidence writes â€” payments, destructive deletes, heavy validation â€” I keep an honest pending state. Showing success you can't guarantee erodes trust faster than a spinner.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://www.devya.dev/blogs/useoptimistic-react-19-patterns-pitfalls" rel="noopener noreferrer"&gt;devya.dev&lt;/a&gt;. Also on &lt;a href="https://www.eng-ahmed.com/blog/useoptimistic-react-19-patterns-pitfalls" rel="noopener noreferrer"&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a href="https://www.devya.dev" rel="noopener noreferrer"&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
