Best Practices

AI Rules for React Server Components

AI adds 'use client' to every file and fetches data in useEffect. Rules for server-first architecture, client boundary placement, server actions, streaming with Suspense, and data fetching patterns.

8 min read·February 16, 2025

'use client' on every file, useEffect for all data, zero streaming — an SPA wearing RSC clothes

Server-first defaults, narrow client boundaries, server actions, Suspense streaming, parallel fetching

AI Turns Server Components into Client Components

AI generates React Server Component code with: 'use client' on every file (converting the entire app to client-side rendering), useEffect for data fetching (waterfall: render, then fetch, then render again), no streaming (users wait for all data before seeing anything), client-side state for server data (useState + useEffect instead of async component), and no server actions (API routes for every form submission). The result: a Next.js App Router project that behaves like a Create React App SPA — all the complexity of RSC with none of the benefits.

Modern RSC architecture is: server-first (components are server by default — zero client JS), boundary-minimal ('use client' only on interactive leaves, not entire pages), action-powered (server actions for mutations — no API route boilerplate), streaming (Suspense boundaries show content as it becomes available), and waterfall-free (parallel async data fetching in server components). AI generates none of these.

These rules cover: server-first component architecture, narrow 'use client' boundary placement, server actions for form mutations, Suspense streaming patterns, and parallel data fetching strategies.

Rule 1: Server Components as the Default

The rule: 'Every component is a Server Component unless it needs interactivity. Server Components: fetch data directly (async function Page() { const users = await db.select().from(usersTable); }), access the database without an API layer, send zero JavaScript to the client, and render on the server with full access to Node.js APIs. Only add 'use client' when the component needs: useState, useEffect, event handlers (onClick, onChange), browser APIs (window, document), or third-party client libraries.'

For the mental model: 'Think of the component tree as a server tree with client leaves. The page layout, data fetching, and static content are server components (zero JS shipped). Interactive widgets (buttons, forms, dropdowns, modals) are client components at the leaves. The page component fetches data and passes it as props to client components — the client component receives pre-fetched data, no useEffect needed.'

AI generates: 'use client' at the top of page.tsx — the entire page is a client component. Data fetching moves to useEffect (waterfall), the database cannot be accessed directly (need an API route), and all component JS is shipped to the client. Remove 'use client': the page becomes a server component, data fetching is direct and parallel, zero JS shipped, and the page renders faster.

  • Server Component = default — zero client JS, direct data access, async/await
  • 'use client' only for: useState, useEffect, onClick, browser APIs, client libraries
  • Server tree with client leaves — layout and data are server, interactions are client
  • Page fetches data, passes props to client components — no useEffect needed
  • Remove 'use client' from page.tsx — instant performance improvement
💡 Remove 'use client', Instant Win

AI puts 'use client' on page.tsx — the entire page ships as client JS. Remove it: the page becomes a Server Component with direct data access, zero client JS, and faster rendering. One line deleted, instant performance improvement.

Rule 2: Narrow 'use client' Boundaries

The rule: 'Push 'use client' as far down the component tree as possible. Do not mark a page or layout as 'use client' — mark only the interactive leaf components. A page with a like button: the page is a server component (fetches article, renders content), the LikeButton is a client component (needs onClick and useState). Only LikeButton ships JS to the client — the rest of the page is zero JS.'

For the extraction pattern: 'When a server component needs one interactive element, extract it: // page.tsx (Server Component) export default async function ArticlePage() { const article = await getArticle(); return (<article><h1>{article.title}</h1><p>{article.content}</p><LikeButton articleId={article.id} initialCount={article.likes} /></article>); } // LikeButton.tsx 'use client' — only this file ships JS. The page is 5KB of HTML; the LikeButton is 2KB of JS. Without extraction: the entire page is 50KB of JS.'

AI generates: 'use client' on the page because it contains one onClick handler. The entire page — data fetching, layout, content, and all children — becomes client-side. Extract the interactive part into a small client component: 95% of the page stays server-rendered (zero JS), 5% is a tiny client component. Same functionality, dramatically less client JavaScript.

Rule 3: Server Actions for Mutations

The rule: 'Use server actions for form submissions and mutations instead of API routes. A server action is an async function marked with 'use server' that runs on the server when called from the client. Pattern: async function createComment(formData: FormData) { 'use server'; const content = formData.get('content'); await db.insert(comments).values({ content }); revalidatePath('/article'); }. Usage: <form action={createComment}>. No API route, no fetch call, no request/response parsing.'

For progressive enhancement: 'Server actions with <form action={...}> work without JavaScript — the form submits as a standard HTML form submission. When JS is available, the submission is enhanced: no page reload, optimistic UI, pending states via useFormStatus(). This is progressive enhancement: the core functionality works for everyone, JS enhances the experience for those who have it.'

AI generates: an API route (/api/comments POST), a fetch call in the client component, JSON serialization/deserialization, error handling for the fetch, and loading state management. Server actions: one function, one form action attribute. The framework handles serialization, error boundaries, revalidation, and optimistic updates. Five files reduced to one function.

  • 'use server' functions for mutations — no API route boilerplate
  • <form action={serverAction}> — works without JavaScript (progressive enhancement)
  • useFormStatus() for pending states — useOptimistic() for optimistic UI
  • revalidatePath/revalidateTag after mutation — automatic cache invalidation
  • Server action = one function vs API route = route file + fetch + serialize + error handling
⚠️ Five Files to One Function

API route pattern: route file + fetch call + JSON serialize + error handling + loading state = five moving parts. Server action: one async function with 'use server', one <form action={fn}>. Framework handles serialization, errors, revalidation. Five files reduced to one function.

Rule 4: Suspense Streaming for Progressive Rendering

The rule: 'Wrap slow data fetches in Suspense boundaries to stream content progressively. The page shell renders immediately; data-dependent sections stream in as they resolve. Pattern: <Suspense fallback={<CommentsSkeleton />}><Comments articleId={id} /></Suspense>. The article content, header, and sidebar render instantly. Comments (slow database query) stream in when ready. The user sees content immediately instead of waiting for everything.'

For parallel streaming: 'Multiple Suspense boundaries stream independently: <div><Suspense fallback={<ArticleSkeleton />}><Article id={id} /></Suspense><Suspense fallback={<CommentsSkeleton />}><Comments articleId={id} /></Suspense><Suspense fallback={<RelatedSkeleton />}><RelatedArticles articleId={id} /></Suspense></div>. Three data fetches run in parallel on the server. Each section streams to the client as its data resolves — no waterfall, no blocking.'

AI generates: one big data fetch that blocks the entire page. The user waits 2 seconds for comments + related articles + recommendations before seeing anything. With Suspense streaming: the article renders in 200ms, comments stream in at 800ms, related articles at 1.2s. The user reads the article while secondary content loads. Same total time, dramatically better perceived performance.

ℹ️ Read While Loading

Without streaming: user waits 2 seconds for all data before seeing anything. With Suspense streaming: article renders in 200ms, comments stream at 800ms, related articles at 1.2s. User reads the article while secondary content loads. Same total time, dramatically better perceived performance.

Rule 5: Parallel Data Fetching Without Waterfalls

The rule: 'In server components, fetch data in parallel, not sequentially. Waterfall: const user = await getUser(id); const posts = await getPosts(user.id); const comments = await getComments(posts[0].id); — each fetch waits for the previous. Parallel: const [user, posts, comments] = await Promise.all([getUser(id), getPosts(id), getComments(id)]); — all three fetch simultaneously. Total time: max(individual times) instead of sum(individual times).'

For dependent fetches: 'When fetch B depends on fetch A result, you cannot parallelize them — but you can parallelize everything else. Pattern: const user = await getUser(id); const [posts, settings, notifications] = await Promise.all([getPosts(user.id), getSettings(user.id), getNotifications(user.id)]); — sequential where required (user first), parallel where possible (posts, settings, notifications together).'

AI generates: five sequential awaits in a server component — each 200ms, total 1 second. Three of the five are independent. Promise.all for the independent three: 200ms (user) + 200ms (parallel three) + 200ms (dependent fifth) = 600ms. Same data, 40% faster. In server components, this optimization is free — no client-side caching library needed, just Promise.all.

Complete React Server Components Rules Template

Consolidated rules for React Server Components.

  • Server Component = default — 'use client' only for interactivity (useState, onClick)
  • Push 'use client' to leaves — extract interactive bits, keep pages server-rendered
  • Server actions for mutations — 'use server' functions, no API route boilerplate
  • Progressive enhancement — <form action={...}> works without JavaScript
  • Suspense streaming — show content progressively, not all-or-nothing
  • Parallel data fetching — Promise.all for independent fetches, sequential only when dependent
  • No useEffect for data — async server components fetch directly, no waterfall
  • revalidatePath/revalidateTag — cache invalidation after server action mutations