<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>App Router on RockB</title><link>https://baeseokjae.github.io/tags/app-router/</link><description>Recent content in App Router on RockB</description><image><title>RockB</title><url>https://baeseokjae.github.io/images/og-default.png</url><link>https://baeseokjae.github.io/images/og-default.png</link></image><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 02 Jun 2026 03:48:36 +0000</lastBuildDate><atom:link href="https://baeseokjae.github.io/tags/app-router/index.xml" rel="self" type="application/rss+xml"/><item><title>React Server Components in Next.js App Router: Complete Developer Guide</title><link>https://baeseokjae.github.io/posts/react-server-components-nextjs-app-router-guide-2026/</link><pubDate>Tue, 02 Jun 2026 03:48:36 +0000</pubDate><guid>https://baeseokjae.github.io/posts/react-server-components-nextjs-app-router-guide-2026/</guid><description>Master React Server Components in Next.js App Router — data fetching, streaming, Server Actions, PPR, and migration from Pages Router.</description><content:encoded><![CDATA[<p>React Server Components (RSC) are components that run exclusively on the server, never ship JavaScript to the browser, and can access databases and file systems directly. In Next.js 15 App Router, every component in the <code>app/</code> directory is a Server Component by default — you opt into client-side interactivity with <code>'use client'</code>, not out of it. This guide covers the complete RSC mental model, data fetching patterns, streaming, Server Actions, caching, Partial Prerendering, and the 7 mistakes that silently wreck bundle size.</p>
<h2 id="what-are-react-server-components-the-mental-model-that-changes-everything">What Are React Server Components? (The Mental Model That Changes Everything)</h2>
<p>React Server Components are a fundamentally new rendering primitive introduced in React 18 and productionized in Next.js 13+ App Router. An RSC renders entirely on the server — once — and sends its output as a serialized React tree (not HTML, not JSON) over the wire. The browser never receives the component&rsquo;s JavaScript. It never re-renders. It never hydrates. This is categorically different from Server-Side Rendering (SSR), which sends HTML but still ships the full component JavaScript bundle for hydration. With RSC, non-interactive UI — product descriptions, blog posts, nav trees, data tables — never appears in the client bundle at all. Applications that fully adopt RSC patterns consistently report <strong>50–70% reductions in First Load JS</strong> and significant Largest Contentful Paint (LCP) improvements (vladimirsiedykh.com, 2025). A component that fetches from a database and renders read-only HTML simply disappears from the client bundle entirely. That is the mental model shift: the default changed from &ldquo;client unless proven server&rdquo; to &ldquo;server unless you need the browser.&rdquo;</p>
<p>The key distinction from SSR: SSR pre-renders HTML on the server and then hydrates the same component on the client (downloading its JS). RSC components run on the server only — no hydration, no client JS. The two can coexist on the same page: a Server Component wraps a Client Component that hydrates normally.</p>
<h3 id="why-most-developers-havent-made-the-shift">Why Most Developers Haven&rsquo;t Made the Shift</h3>
<p>Despite more than 50% of developers expressing positive sentiment about RSC, only <strong>29% have actually used them</strong> — a massive awareness-adoption gap (State of React 2025). The friction is real: the mental model requires unlearning 10 years of SPA instincts, the Context API doesn&rsquo;t work across the server-client boundary, and the error messages when you mix them incorrectly can be cryptic. This guide exists to close that gap.</p>
<h2 id="server-components-vs-client-components-the-decision-framework">Server Components vs Client Components: The Decision Framework</h2>
<p>Server Components and Client Components are not better or worse — they answer different questions. Use a Server Component when you need to fetch data from a database, read environment secrets, access the file system, reduce client bundle size, or render static/read-only UI. Use <code>'use client'</code> when you need <code>useState</code>, <code>useEffect</code>, event handlers (<code>onClick</code>, <code>onChange</code>), browser APIs (<code>localStorage</code>, <code>window</code>, geolocation), or React Query / SWR for client-side caching. In Next.js App Router, all components in <code>app/</code> are Server Components by default — you only add <code>'use client'</code> when you genuinely need the browser. The most expensive mistake developers make is adding <code>'use client'</code> at the top of a large component tree because one leaf needs interactivity. That pushes the entire subtree into the client bundle. The correct pattern: push <code>'use client'</code> as deep as the tree as possible, to the single interactive leaf, and keep all parent containers as Server Components.</p>
<p><strong>Decision table:</strong></p>
<table>
  <thead>
      <tr>
          <th>Need</th>
          <th>Server Component</th>
          <th>Client Component</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Database / ORM queries</td>
          <td>✅</td>
          <td>❌</td>
      </tr>
      <tr>
          <td>Secrets / API keys</td>
          <td>✅</td>
          <td>❌</td>
      </tr>
      <tr>
          <td><code>useState</code> / <code>useReducer</code></td>
          <td>❌</td>
          <td>✅</td>
      </tr>
      <tr>
          <td><code>useEffect</code></td>
          <td>❌</td>
          <td>✅</td>
      </tr>
      <tr>
          <td>Event handlers</td>
          <td>❌</td>
          <td>✅</td>
      </tr>
      <tr>
          <td>Browser APIs</td>
          <td>❌</td>
          <td>✅</td>
      </tr>
      <tr>
          <td><code>async/await</code> in component body</td>
          <td>✅</td>
          <td>❌</td>
      </tr>
      <tr>
          <td>Can render other Server Components</td>
          <td>✅</td>
          <td>❌ (import only Client or pass as <code>children</code>)</td>
      </tr>
  </tbody>
</table>
<h3 id="the-donut-pattern-server-inside-client">The &ldquo;Donut&rdquo; Pattern: Server Inside Client</h3>
<p>You cannot import a Server Component inside a <code>'use client'</code> module — the import graph seals it into the client bundle. But you <strong>can</strong> pass a Server Component as <code>children</code> or a prop to a Client Component. This is the &ldquo;donut&rdquo; pattern: the Client Component is the donut (with a hole), and a Server Component fills the hole.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#75715e">// ❌ Wrong: imports RSC inside client module
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#39;use client&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">ServerSidebar</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;./ServerSidebar&#39;</span> <span style="color:#75715e">// now bundled for client
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ✅ Correct: pass as children
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// layout.tsx (Server Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">ClientShell</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;./ClientShell&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">ServerSidebar</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;./ServerSidebar&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Layout() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">ClientShell</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">ServerSidebar</span> /&gt; {<span style="color:#75715e">/* stays server-only */</span>}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">ClientShell</span>&gt;
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="setting-up-your-nextjs-app-router-project">Setting Up Your Next.js App Router Project</h2>
<p>Getting started with the App Router requires Next.js 13.4+ (stable), though 15.x is recommended for the full RSC, Server Actions, and PPR feature set. Create a new project with <code>npx create-next-app@latest --app</code> and choose TypeScript. The resulting structure places all routes under <code>app/</code>, with <code>page.tsx</code> as the route entry, <code>layout.tsx</code> for shared shells, <code>loading.tsx</code> for Suspense fallbacks, and <code>error.tsx</code> for error boundaries. Every file in <code>app/</code> is a Server Component by default — no annotation needed. The <code>public/</code> directory, <code>next.config.ts</code>, and <code>tailwind.config.ts</code> live at the root. TypeScript strict mode is enabled by default in new projects. Turbopack is now the default bundler in Next.js 15, delivering <strong>10x faster HMR and 4x faster production builds</strong> compared to webpack — you get this for free on new projects.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npx create-next-app@latest my-app <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --typescript --tailwind --eslint --app <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --src-dir --import-alias <span style="color:#e6db74">&#34;@/*&#34;</span>
</span></span><span style="display:flex;"><span>cd my-app <span style="color:#f92672">&amp;&amp;</span> npm run dev
</span></span></code></pre></div><h3 id="file-conventions-you-must-know">File Conventions You Must Know</h3>
<table>
  <thead>
      <tr>
          <th>File</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>layout.tsx</code></td>
          <td>Shared UI wrapper (not remounted on navigation)</td>
      </tr>
      <tr>
          <td><code>page.tsx</code></td>
          <td>Route leaf, publicly addressable</td>
      </tr>
      <tr>
          <td><code>loading.tsx</code></td>
          <td>Auto-wrapped Suspense fallback for the route segment</td>
      </tr>
      <tr>
          <td><code>error.tsx</code></td>
          <td>Error boundary for the route segment (must be Client Component)</td>
      </tr>
      <tr>
          <td><code>not-found.tsx</code></td>
          <td>404 UI for the segment</td>
      </tr>
      <tr>
          <td><code>route.ts</code></td>
          <td>API endpoint (replaces <code>pages/api/</code>)</td>
      </tr>
      <tr>
          <td><code>template.tsx</code></td>
          <td>Like layout but remounts on navigation</td>
      </tr>
  </tbody>
</table>
<h2 id="data-fetching-patterns-sequential-vs-parallel-vs-streaming">Data Fetching Patterns: Sequential vs Parallel vs Streaming</h2>
<p>Data fetching in the App Router happens directly in <code>async</code> Server Components — no <code>getServerSideProps</code>, no custom hooks, no API layer needed for internal data. The <code>fetch()</code> function is extended by Next.js to support request deduplication and caching. The three patterns every developer needs: <strong>sequential</strong> (when request B depends on response A — unavoidable but keep it shallow), <strong>parallel</strong> (when requests are independent — always prefer), and <strong>streaming</strong> (when some data is slow and you want to unblock the rest of the UI). Sequential fetching is the default trap: <code>const user = await getUser(); const posts = await getPostsByUser(user.id)</code> serializes the requests, adding latency for every hop. For independent data, use <code>Promise.all()</code> to parallelize.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#75715e">// ❌ Sequential — slow
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">UserDashboard</span>({ <span style="color:#a6e22e">userId</span> }<span style="color:#f92672">:</span> { <span style="color:#a6e22e">userId</span>: <span style="color:#66d9ef">string</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">user</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getUser</span>(<span style="color:#a6e22e">userId</span>)         <span style="color:#75715e">// 120ms
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">posts</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getPostsByUser</span>(<span style="color:#a6e22e">userId</span>) <span style="color:#75715e">// +85ms = 205ms total
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">div</span>&gt;...&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ✅ Parallel — fast
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">UserDashboard</span>({ <span style="color:#a6e22e">userId</span> }<span style="color:#f92672">:</span> { <span style="color:#a6e22e">userId</span>: <span style="color:#66d9ef">string</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">user</span>, <span style="color:#a6e22e">posts</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">Promise</span>.<span style="color:#a6e22e">all</span>([
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">getUser</span>(<span style="color:#a6e22e">userId</span>),       <span style="color:#75715e">// 120ms
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">getPostsByUser</span>(<span style="color:#a6e22e">userId</span>) <span style="color:#75715e">// 85ms (runs concurrently)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  ])                       <span style="color:#75715e">// = 120ms total
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">div</span>&gt;...&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="sibling-server-components-for-parallelism">Sibling Server Components for Parallelism</h3>
<p>The cleanest way to parallelize in React is <strong>sibling Server Components in separate Suspense boundaries</strong>. Each component fetches its own data independently, and React streams them as they resolve — no <code>Promise.all</code> needed at the parent level.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#75715e">// Each section fetches independently and streams as ready
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Dashboard() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">MetricsSkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">MetricsPanel</span> /&gt;   {<span style="color:#75715e">/* fetches /api/metrics */</span>}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">ActivitySkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">ActivityFeed</span> /&gt;   {<span style="color:#75715e">/* fetches /api/activity */</span>}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/&gt;
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="streaming-with-suspense-making-slow-data-invisible-to-users">Streaming with Suspense: Making Slow Data Invisible to Users</h2>
<p>Streaming is the RSC feature that changes how users experience slow pages. Instead of waiting for every data fetch before sending any HTML, Next.js sends the static shell immediately and streams in dynamic sections as their data resolves. The result: users see meaningful content within milliseconds even if some API calls take 500ms+. Streaming uses the standard React <code>&lt;Suspense&gt;</code> boundary — wrap any slow Server Component in <code>&lt;Suspense fallback={&lt;Skeleton /&gt;}&gt;</code> and Next.js handles the rest. The <code>loading.tsx</code> file in any route segment automatically creates a top-level Suspense boundary for that entire segment. For granular control, add Suspense boundaries at the component level. Server Components can yield their output as a stream; Client Components hydrate after their corresponding HTML chunk arrives. In practice, streaming transforms a 1.2s white screen into a 100ms skeleton-to-content progression — a measurable conversion improvement for e-commerce and SaaS dashboards.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#75715e">// app/dashboard/page.tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Suspense</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;react&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">RevenueChart</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@/components/RevenueChart&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">RecentSales</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@/components/RecentSales&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">DashboardPage() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">h1</span>&gt;<span style="color:#a6e22e">Dashboard</span>&lt;/<span style="color:#f92672">h1</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* Streams in as revenue data resolves */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">ChartSkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">RevenueChart</span> /&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* Streams in independently — doesn&#39;t wait for chart */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">SalesSkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">RecentSales</span> /&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="loadingtsx-vs-manual-suspense">Loading.tsx vs Manual Suspense</h3>
<p><code>loading.tsx</code> wraps the entire page segment in Suspense automatically. Use it for page-level skeletons. Use manual <code>&lt;Suspense&gt;</code> at the component level when different sections of a page have different data speeds — the fast sections shouldn&rsquo;t wait for the slow ones.</p>
<h2 id="server-actions-replacing-api-routes-for-mutations">Server Actions: Replacing API Routes for Mutations</h2>
<p>Server Actions are async functions that run on the server but can be called directly from Client Components — including from <code>&lt;form action={...}&gt;</code> and from <code>onClick</code> handlers. They replace the traditional pattern of writing a <code>POST /api/...</code> route handler, calling it with <code>fetch()</code> from the client, and managing loading/error state manually. A Server Action is marked with <code>'use server'</code> at the top of the function body (or at the top of a file to mark all exports as server actions). Next.js wires the RPC call automatically. Critically, <code>'use server'</code> is <strong>not the same as &ldquo;Server Component&rdquo;</strong> — it&rsquo;s a directive that tells the bundler to extract this function into a server endpoint callable from the client. Server Actions integrate natively with form&rsquo;s <code>action</code> prop, <code>useActionState</code> hook (React 19), and Next.js <code>revalidatePath</code>/<code>revalidateTag</code> for cache invalidation — making full-stack mutations a first-class pattern without a separate API layer.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#75715e">// app/actions.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#39;use server&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">revalidatePath</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;next/cache&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">db</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@/lib/db&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">createPost</span>(<span style="color:#a6e22e">formData</span>: <span style="color:#66d9ef">FormData</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">title</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">formData</span>.<span style="color:#66d9ef">get</span>(<span style="color:#e6db74">&#39;title&#39;</span>) <span style="color:#66d9ef">as</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">create</span>({ <span style="color:#a6e22e">data</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">title</span> } })
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">revalidatePath</span>(<span style="color:#e6db74">&#39;/posts&#39;</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// app/posts/new/page.tsx (Server Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">createPost</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@/app/actions&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">NewPostPage() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">form</span> <span style="color:#a6e22e">action</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">createPost</span>}&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">input</span> <span style="color:#a6e22e">name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;title&#34;</span> <span style="color:#a6e22e">placeholder</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Post title&#34;</span> /&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;submit&#34;</span>&gt;<span style="color:#a6e22e">Create</span>&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">form</span>&gt;
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="server-actions-vs-api-routes-when-to-use-each">Server Actions vs API Routes: When to Use Each</h3>
<p>Use Server Actions for form submissions, CRUD mutations from Client Components, and any mutation that should revalidate cached data in Next.js. Use <code>route.ts</code> API routes when you need to expose an endpoint to external consumers (mobile apps, third-party services, webhooks), when you need fine-grained HTTP control (status codes, headers, streaming responses), or when building a public REST API. For internal Next.js mutations, Server Actions eliminate the client/server boilerplate entirely.</p>
<h2 id="caching-in-the-app-router-fetch-unstable_cache-and-use-cache">Caching in the App Router: fetch(), unstable_cache, and use cache</h2>
<p>Next.js App Router has one of the most powerful and one of the most confusing caching systems in frontend frameworks. There are four layers: <strong>Request Memoization</strong> (deduplicates identical <code>fetch()</code> calls within a single render), <strong>Data Cache</strong> (persists <code>fetch()</code> responses across requests — opt out with <code>{ cache: 'no-store' }</code>), <strong>Full Route Cache</strong> (caches rendered HTML at build time for static routes), and <strong>Router Cache</strong> (client-side cache of visited route segments). By default, <code>fetch()</code> in Server Components is cached indefinitely — which surprised many developers upgrading from Pages Router. Next.js 15 changed the default to <code>no-store</code> for <code>fetch()</code> to reduce confusion. For database queries and ORM calls that don&rsquo;t go through <code>fetch()</code>, use <code>unstable_cache</code> (stable in Next.js 15 despite the name) or the new <code>use cache</code> directive (experimental in 15.x). Tag-based invalidation with <code>revalidateTag()</code> is the most surgical caching approach: tag your cached data, then invalidate exactly those tags from a Server Action when data changes.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">unstable_cache</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;next/cache&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Cached for 1 hour, tagged for invalidation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">getProductById</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">unstable_cache</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">async</span> (<span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">product</span>.<span style="color:#a6e22e">findUnique</span>({ <span style="color:#a6e22e">where</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">id</span> } }),
</span></span><span style="display:flex;"><span>  [<span style="color:#e6db74">&#39;product&#39;</span>],
</span></span><span style="display:flex;"><span>  { <span style="color:#a6e22e">revalidate</span>: <span style="color:#66d9ef">3600</span>, <span style="color:#a6e22e">tags</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;products&#39;</span>] }
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Server Action to invalidate
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#39;use server&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">revalidateTag</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;next/cache&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">updateProduct</span>(<span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">data</span>: <span style="color:#66d9ef">Partial</span>&lt;<span style="color:#f92672">Product</span>&gt;) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">product</span>.<span style="color:#a6e22e">update</span>({ <span style="color:#a6e22e">where</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">id</span> }, <span style="color:#a6e22e">data</span> })
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">revalidateTag</span>(<span style="color:#e6db74">&#39;products&#39;</span>) <span style="color:#75715e">// clears all cached queries tagged &#39;products&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><h3 id="the-four-caching-layers-at-a-glance">The Four Caching Layers at a Glance</h3>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>What It Caches</th>
          <th>Duration</th>
          <th>Opt Out</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Request Memoization</td>
          <td><code>fetch()</code> within single render</td>
          <td>Single request</td>
          <td>Automatic</td>
      </tr>
      <tr>
          <td>Data Cache</td>
          <td><code>fetch()</code> responses</td>
          <td>Persistent</td>
          <td><code>cache: 'no-store'</code></td>
      </tr>
      <tr>
          <td>Full Route Cache</td>
          <td>Rendered HTML</td>
          <td>Until redeploy / revalidate</td>
          <td><code>export const dynamic = 'force-dynamic'</code></td>
      </tr>
      <tr>
          <td>Router Cache</td>
          <td>Client-side route segments</td>
          <td>Session / 30s (dynamic)</td>
          <td><code>router.refresh()</code></td>
      </tr>
  </tbody>
</table>
<h2 id="partial-prerendering-ppr-the-best-of-static-and-dynamic">Partial Prerendering (PPR): The Best of Static and Dynamic</h2>
<p>Partial Prerendering (PPR) is Next.js&rsquo;s answer to the long-standing tradeoff between static (fast, stale) and dynamic (slow, fresh) rendering. With PPR, a single route can serve a <strong>static shell from the CDN edge</strong> (TTFB under 50ms) while <strong>streaming dynamic sections</strong> on request as they resolve — on the same page, with no layout shift. The static shell is pre-rendered at build time and cached at the edge; each Suspense boundary marks a &ldquo;hole&rdquo; that fills dynamically. This is fundamentally different from ISR (Incremental Static Regeneration), which regenerates the whole page. PPR regenerates nothing — the shell is always static, the holes are always fresh. Enable PPR in <code>next.config.ts</code> with <code>experimental: { ppr: true }</code> (stable in Next.js 15 for opt-in routes with <code>export const experimental_ppr = true</code>). With PPR, static shell TTFB from CDN edge can be <strong>under 50ms</strong>, while dynamic sections stream in separately on request (samcheek.com, 2026).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// next.config.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#66d9ef">type</span> { <span style="color:#a6e22e">NextConfig</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;next&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">config</span>: <span style="color:#66d9ef">NextConfig</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">experimental</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ppr</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;incremental&#39;</span>, <span style="color:#75715e">// opt-in per route in Next.js 15
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  },
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#a6e22e">config</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// app/product/[id]/page.tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">experimental_ppr</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span> <span style="color:#75715e">// enable PPR for this route
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Suspense</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;react&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">StaticProductInfo</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@/components/StaticProductInfo&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">DynamicInventory</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@/components/DynamicInventory&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ProductPage</span>({ <span style="color:#a6e22e">params</span> }<span style="color:#f92672">:</span> { <span style="color:#a6e22e">params</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span> } }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* Served from edge cache immediately */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">StaticProductInfo</span> <span style="color:#a6e22e">productId</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">id</span>} /&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* Streams in fresh on every request */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">InventorySkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">DynamicInventory</span> <span style="color:#a6e22e">productId</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">id</span>} /&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/&gt;
</span></span><span style="display:flex;"><span>  )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="ppr-vs-isr-vs-static-vs-dynamic">PPR vs ISR vs Static vs Dynamic</h3>
<table>
  <thead>
      <tr>
          <th>Strategy</th>
          <th>TTFB</th>
          <th>Freshness</th>
          <th>When to Use</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Static (<code>force-static</code>)</td>
          <td>~20ms CDN</td>
          <td>Build time</td>
          <td>Marketing pages, docs</td>
      </tr>
      <tr>
          <td>ISR</td>
          <td>~20ms CDN</td>
          <td>Stale up to revalidate period</td>
          <td>Blog posts, product pages</td>
      </tr>
      <tr>
          <td>PPR</td>
          <td>~50ms CDN</td>
          <td>Shell static, holes fresh</td>
          <td>Mixed static/dynamic per page</td>
      </tr>
      <tr>
          <td>Dynamic (<code>force-dynamic</code>)</td>
          <td>~200ms+ origin</td>
          <td>Always fresh</td>
          <td>Dashboards, user-specific pages</td>
      </tr>
  </tbody>
</table>
<h2 id="7-common-react-server-component-mistakes-and-how-to-fix-them">7 Common React Server Component Mistakes (and How to Fix Them)</h2>
<p>These are the seven mistakes that senior Next.js developers catch in code review most often. Each silently increases bundle size, breaks builds at runtime, or creates subtle correctness issues that only appear in production. The RSC model is strict about the server/client boundary — the bundler enforces it, not the developer. Crossing the boundary incorrectly produces some of the least actionable error messages in the React ecosystem. Understanding these patterns in advance prevents hours of debugging.</p>
<p><strong>Mistake 1: Putting <code>'use client'</code> on a parent that contains only one interactive leaf.</strong></p>
<p>The fix: extract the single interactive element into its own Client Component file. The parent stays server-side and keeps all its data fetching out of the bundle.</p>
<p><strong>Mistake 2: Importing a Server Component inside a <code>'use client'</code> module.</strong></p>
<p>This silently promotes the Server Component to a Client Component, bundling its code and losing its data fetching benefits. Fix: pass as <code>children</code> prop (the donut pattern).</p>
<p><strong>Mistake 3: Using the Context API across the server-client boundary.</strong></p>
<p>Context is a client-side mechanism. Providers must live in Client Components. The most common pattern is a <code>Providers</code> Client Component wrapping the app in <code>layout.tsx</code> with all client providers.</p>
<p><strong>Mistake 4: Accessing <code>cookies()</code>, <code>headers()</code>, or <code>searchParams</code> inside a cached component.</strong></p>
<p>These are dynamic APIs. Calling them inside a statically cached route will throw at build time. Either mark the route <code>force-dynamic</code> or move the dynamic read to the nearest Suspense-bounded Server Component.</p>
<p><strong>Mistake 5: Passing non-serializable props (functions, class instances) from Server to Client Components.</strong></p>
<p>Props cross the wire as JSON. Functions, Dates (use ISO strings instead), class instances, and <code>undefined</code> values throw serialization errors. Use <code>Date.toISOString()</code>, plain objects, and primitives only.</p>
<p><strong>Mistake 6: Creating waterfall fetches inside a single async Server Component.</strong></p>
<p>Sequential <code>await</code> calls are fine when the second depends on the first, but avoid them when requests are independent. Use <code>Promise.all()</code> or sibling Suspense components for parallel execution.</p>
<p><strong>Mistake 7: Ignoring cache semantics and fetching fresh data that should be cached.</strong></p>
<p>The inverse problem: marking everything <code>no-store</code> when most data could be cached and revalidated on demand. Use <code>unstable_cache</code> with <code>revalidateTag()</code> for database queries and set explicit <code>revalidate</code> values.</p>
<h2 id="testing-server-components-and-server-actions">Testing Server Components and Server Actions</h2>
<p>Testing React Server Components requires a different approach from standard component testing because they&rsquo;re async and run in a Node.js environment, not a browser. As of 2026, the testing ecosystem is catching up but is not fully mature — testing gaps remain a significant adoption barrier for some teams. For Server Components, use <strong>Vitest with React Testing Library</strong> (RTL) version 15+, which added async component rendering support, or <strong>Playwright</strong> for end-to-end tests that test the full rendering pipeline including streaming. For Server Actions, you can unit test them directly in Vitest by importing and calling them with a mocked database — they&rsquo;re just async functions. Jest with <code>@testing-library/react</code> requires the <code>experimental-vm-modules</code> flag for ESM support. The recommended setup in 2026 is Vitest + RTL for unit/integration and Playwright for end-to-end.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#75715e">// __tests__/ProductCard.test.tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">render</span>, <span style="color:#a6e22e">screen</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@testing-library/react&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">describe</span>, <span style="color:#a6e22e">it</span>, <span style="color:#a6e22e">expect</span>, <span style="color:#a6e22e">vi</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;vitest&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">ProductCard</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@/components/ProductCard&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Mock the database call
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">vi</span>.<span style="color:#a6e22e">mock</span>(<span style="color:#e6db74">&#39;@/lib/db&#39;</span>, () <span style="color:#f92672">=&gt;</span> ({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">db</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">product</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">findUnique</span>: <span style="color:#66d9ef">vi.fn</span>().<span style="color:#a6e22e">mockResolvedValue</span>({ <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;1&#39;</span>, <span style="color:#a6e22e">name</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Widget&#39;</span>, <span style="color:#a6e22e">price</span>: <span style="color:#66d9ef">29.99</span> }) } }
</span></span><span style="display:flex;"><span>}))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">describe</span>(<span style="color:#e6db74">&#39;ProductCard&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">it</span>(<span style="color:#e6db74">&#39;renders product details&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">jsx</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">ProductCard</span>({ <span style="color:#a6e22e">productId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;1&#39;</span> })
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">render</span>(<span style="color:#a6e22e">jsx</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByText</span>(<span style="color:#e6db74">&#39;Widget&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">screen</span>.<span style="color:#a6e22e">getByText</span>(<span style="color:#e6db74">&#39;$29.99&#39;</span>)).<span style="color:#a6e22e">toBeInTheDocument</span>()
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h3 id="testing-server-actions-directly">Testing Server Actions Directly</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx" data-lang="tsx"><span style="display:flex;"><span><span style="color:#75715e">// __tests__/actions.test.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">describe</span>, <span style="color:#a6e22e">it</span>, <span style="color:#a6e22e">expect</span>, <span style="color:#a6e22e">vi</span>, <span style="color:#a6e22e">beforeEach</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;vitest&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">createPost</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@/app/actions&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">revalidatePath</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;next/cache&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">vi</span>.<span style="color:#a6e22e">mock</span>(<span style="color:#e6db74">&#39;next/cache&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">vi</span>.<span style="color:#a6e22e">mock</span>(<span style="color:#e6db74">&#39;@/lib/db&#39;</span>, () <span style="color:#f92672">=&gt;</span> ({ <span style="color:#a6e22e">db</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">post</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">create</span>: <span style="color:#66d9ef">vi.fn</span>() } } }))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">describe</span>(<span style="color:#e6db74">&#39;createPost&#39;</span>, () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">it</span>(<span style="color:#e6db74">&#39;creates post and revalidates path&#39;</span>, <span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">formData</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">FormData</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">formData</span>.<span style="color:#a6e22e">append</span>(<span style="color:#e6db74">&#39;title&#39;</span>, <span style="color:#e6db74">&#39;Test Post&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">createPost</span>(<span style="color:#a6e22e">formData</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">expect</span>(<span style="color:#a6e22e">revalidatePath</span>).<span style="color:#a6e22e">toHaveBeenCalledWith</span>(<span style="color:#e6db74">&#39;/posts&#39;</span>)
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><h2 id="migrating-from-pages-router-to-app-router">Migrating from Pages Router to App Router</h2>
<p>Migrating a production Next.js application from Pages Router to App Router is incremental — Next.js explicitly supports running both routers in the same application. The Pages Router at <code>pages/</code> and the App Router at <code>app/</code> coexist. Start by moving layouts: the <code>_app.tsx</code> global wrapper becomes <code>app/layout.tsx</code>, and <code>_document.tsx</code> disappears (its functionality is built into the root layout). Route files move from <code>pages/route.tsx</code> to <code>app/route/page.tsx</code>. <code>getServerSideProps</code> and <code>getStaticProps</code> are replaced by <code>async</code> Server Components that fetch directly. <code>getStaticPaths</code> becomes <code>generateStaticParams</code>. API routes move from <code>pages/api/</code> to <code>app/api/route.ts</code> with named HTTP method exports. The biggest mental model shift is middleware and <code>useRouter</code> — import <code>useRouter</code> from <code>next/navigation</code> instead of <code>next/router</code>, and <code>router.push</code> behavior differences around shallow routing require careful testing.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// Before: pages/products/[id].tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getServerSideProps</span>({ <span style="color:#a6e22e">params</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">product</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getProduct</span>(<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">id</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> { <span style="color:#a6e22e">props</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">product</span> } }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ProductPage</span>({ <span style="color:#a6e22e">product</span> }) { ... }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// After: app/products/[id]/page.tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ProductPage</span>({ <span style="color:#a6e22e">params</span> }<span style="color:#f92672">:</span> { <span style="color:#a6e22e">params</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span> } }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">product</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getProduct</span>(<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">id</span>) <span style="color:#75715e">// fetch directly
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">ProductView</span> <span style="color:#a6e22e">product</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">product</span>} /&gt;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="migration-checklist">Migration Checklist</h3>
<ul>
<li><input disabled="" type="checkbox"> Add <code>app/</code> directory alongside <code>pages/</code> — both coexist</li>
<li><input disabled="" type="checkbox"> Create <code>app/layout.tsx</code> with <code>&lt;html&gt;</code> and <code>&lt;body&gt;</code> tags</li>
<li><input disabled="" type="checkbox"> Migrate routes one by one, starting with the simplest static pages</li>
<li><input disabled="" type="checkbox"> Replace <code>getServerSideProps</code> with <code>async</code> Server Component data fetching</li>
<li><input disabled="" type="checkbox"> Replace <code>getStaticProps</code> + <code>getStaticPaths</code> with Server Component + <code>generateStaticParams</code></li>
<li><input disabled="" type="checkbox"> Move client-side interactivity to leaf <code>'use client'</code> components</li>
<li><input disabled="" type="checkbox"> Update <code>useRouter</code> imports to <code>next/navigation</code></li>
<li><input disabled="" type="checkbox"> Replace <code>pages/api/</code> routes with <code>app/api/route.ts</code> for public endpoints</li>
<li><input disabled="" type="checkbox"> Convert internal mutations to Server Actions</li>
<li><input disabled="" type="checkbox"> Update middleware if you rely on <code>experimental.appDir</code> routing behavior</li>
</ul>
<hr>
<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>
<p>These are the questions developers ask most frequently when adopting React Server Components in Next.js App Router. RSC introduces a new set of invariants — the server/client boundary, serialization rules, caching defaults, and state manager placement — that break intuitions built on the Pages Router and traditional React SPAs. The confusion is compounded by overlapping terminology: &ldquo;Server Component,&rdquo; &ldquo;Server Action,&rdquo; <code>'use server'</code>, and SSR all sound related but mean distinct things. Getting these fundamentals clear eliminates the majority of runtime errors and cryptic build failures teams encounter in their first month with App Router. Each question below addresses a real adoption blocker reported by developers in the State of React 2025 survey and community discussions. Context API incompatibility alone accounts for 59 developer mentions as the single biggest pain point — so understanding the boundary rules is not optional for production teams.</p>
<h3 id="do-i-have-to-use-the-app-router-if-im-on-nextjs-15">Do I have to use the App Router if I&rsquo;m on Next.js 15?</h3>
<p>No. Next.js 15 ships both the App Router and Pages Router, and both receive updates. The Pages Router is not deprecated. The App Router is the recommended direction for new applications, but migration is strictly opt-in and incremental — you can run both simultaneously in the same project during migration.</p>
<h3 id="can-i-use-redux-zustand-or-other-state-managers-with-server-components">Can I use Redux, Zustand, or other state managers with Server Components?</h3>
<p>Yes, but only in Client Components. State managers that rely on <code>useState</code>, context, or browser APIs must live inside <code>'use client'</code> modules. The standard pattern is to keep your state provider in a Client Component wrapper at the root of your layout and access the store only from other Client Components. Server Components have no state — data flows in through props, fetch calls, or direct database reads.</p>
<h3 id="whats-the-difference-between-use-server-and-a-server-component">What&rsquo;s the difference between <code>'use server'</code> and a Server Component?</h3>
<p>A Server Component is any component in the <code>app/</code> directory that does not have <code>'use client'</code> at the top. It renders on the server. <code>'use server'</code> is a directive you add to an <code>async</code> function (or a file of functions) to create a <strong>Server Action</strong> — a server-callable RPC endpoint that Client Components can invoke for mutations. They are different concepts: Server Component = rendering primitive; <code>'use server'</code> = callable server function.</p>
<h3 id="why-did-my-fetch-stop-caching-in-nextjs-15">Why did my <code>fetch()</code> stop caching in Next.js 15?</h3>
<p>Next.js 15 changed the default <code>cache</code> behavior of <code>fetch()</code> from <code>force-cache</code> (cached indefinitely) to <code>no-store</code> (always fresh). This was a breaking change from Next.js 13/14 behavior. To restore caching, explicitly pass <code>{ cache: 'force-cache' }</code> or <code>{ next: { revalidate: 3600 } }</code> to your <code>fetch()</code> calls.</p>
<h3 id="how-do-i-share-data-between-a-server-component-and-a-client-component-without-prop-drilling">How do I share data between a Server Component and a Client Component without prop drilling?</h3>
<p>Use React&rsquo;s <code>cache()</code> function to memoize a data-fetching function, then call it in both the Server Component (for the initial render) and the Client Component (via Server Action). For UI state that only lives client-side, use Zustand or Jotai in Client Components. For server-to-client data that doesn&rsquo;t need reactivity, pass it as serializable props from the parent Server Component to the child Client Component.</p>
]]></content:encoded></item></channel></rss>