DevToolBoxGRATIS
Blogg

React Server Components: Komplett Guide 2026

15 minby DevToolBox

React Server Components (RSC) represent the biggest architectural shift in React since hooks. They allow components to run exclusively on the server, enabling direct database access, zero client-side JavaScript for server components, and automatic code splitting. This guide covers RSC patterns, data fetching strategies, streaming, when to use client vs server components, and practical examples with Next.js App Router.

What Are React Server Components?

React Server Components are components that render on the server and send their output (React elements, not HTML) to the client. Unlike traditional SSR which renders the entire page on the server and hydrates it on the client, Server Components never ship their JavaScript to the browser. This means you can use server-only code (database queries, file system access, API keys) directly in your components without any of that code reaching the client.

In the RSC model, there are three types of components: Server Components (default, run only on server), Client Components (marked with "use client", run on both server and client), and Shared Components (can run in either context depending on who imports them).

Server vs Client Components

AspectServer ComponentClient Component
Default in App RouterYes (default)No (opt-in)
Directive neededNone"use client"
JavaScript sent to clientNone (zero JS)Yes (component + deps)
useState / useEffectNot availableAvailable
Event handlers (onClick)Not availableAvailable
Direct database accessYesNo
File system accessYesNo
Server-only env varsYesNo (only NEXT_PUBLIC_*)
Async component (await)Yes (native async/await)No (use useEffect or SWR)
Bundle size impactZeroAdds to bundle

Decision Tree: Server or Client?

Should this component be a Server or Client Component?

  Does it need useState, useEffect, or useRef?
  ├── YES → Client Component ("use client")
  └── NO ↓

  Does it need event handlers (onClick, onChange, onSubmit)?
  ├── YES → Client Component ("use client")
  └── NO ↓

  Does it use browser-only APIs (window, localStorage, navigator)?
  ├── YES → Client Component ("use client")
  └── NO ↓

  Does it use third-party libraries that require hooks or browser APIs?
  ├── YES → Client Component ("use client")
  └── NO ↓

  Does it fetch data or access databases/file system?
  ├── YES → Server Component (default) ← BEST CHOICE
  └── NO → Server Component (default) ← still the default

RSC Patterns

Pattern 1: Server-Side Data Fetching

Server Components can fetch data directly using async/await. No useEffect, no loading states to manage, no client-side fetch libraries needed.

// app/users/page.tsx — Server Component (default, no directive needed)
import { db } from '@/lib/database';

// This component is async — only possible in Server Components
export default async function UsersPage() {
  // Direct database query — this code NEVER reaches the client
  const users = await db.user.findMany({
    orderBy: { createdAt: 'desc' },
    take: 50,
  });

  return (
    <div>
      <h1>Users ({users.length})</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} — {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

// Benefits:
// - Zero JavaScript sent to client for this component
// - No loading states needed (data is available before render)
// - Database credentials never exposed to the browser
// - No useEffect + fetch + useState dance

Pattern 2: Server-Client Composition

Server Components can import and render Client Components. Client Components cannot import Server Components, but they can accept Server Components as children props.

// components/LikeButton.tsx — Client Component
'use client';
import { useState } from 'react';

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => {
      setLiked(!liked);
      setCount(c => liked ? c - 1 : c + 1);
    }}>
      {liked ? 'Unlike' : 'Like'} ({count})
    </button>
  );
}

// app/posts/[id]/page.tsx — Server Component
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';

export default async function PostPage({ params }: { params: { id: string } }) {
  // Server: fetch data
  const post = await db.post.findUnique({ where: { id: params.id } });

  return (
    <article>
      {/* Server-rendered content (zero JS) */}
      <h1>{post?.title}</h1>
      <div>{post?.content}</div>

      {/* Client Component island (only this ships JS) */}
      <LikeButton postId={params.id} />
    </article>
  );
}

// Pattern: Server Component as children of Client Component
// components/Sidebar.tsx
'use client';
export function Sidebar({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return (
    <aside style={{ display: open ? 'block' : 'none' }}>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {children}  {/* Server Component content passed as children */}
    </aside>
  );
}

Pattern 3: Server Actions for Mutations

Server Actions (marked with "use server") allow Client Components to call server-side functions directly. This replaces API routes for mutations.

// app/actions.ts — Server Action
'use server';

import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.post.create({
    data: { title, content, authorId: 'user-1' },
  });

  revalidatePath('/posts');
}

export async function deletePost(postId: string) {
  await db.post.delete({ where: { id: postId } });
  revalidatePath('/posts');
}

// components/CreatePostForm.tsx — Client Component using Server Action
'use client';
import { createPost } from '@/app/actions';
import { useActionState } from 'react';

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" required />
      <textarea name="content" placeholder="Write your post..." required />
      <button type="submit">Create Post</button>
    </form>
  );
}

// Works without API routes!
// The form submits directly to the server action
// Progressive enhancement: works without JavaScript

Pattern 4: Streaming with Suspense

React Server Components support streaming, which means slow data fetches do not block the entire page. Use Suspense boundaries to stream components as they become ready.

// app/dashboard/page.tsx — Streaming with Suspense
import { Suspense } from 'react';

// Slow component — fetches from external API
async function AnalyticsChart() {
  const data = await fetch('https://api.analytics.com/data', {
    next: { revalidate: 60 },
  });
  const analytics = await data.json();
  return <div>Chart: {analytics.visitors} visitors</div>;
}

// Fast component — fetches from local DB
async function RecentPosts() {
  const posts = await db.post.findMany({ take: 5 });
  return (
    <ul>
      {posts.map(p => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* This renders immediately */}
      <Suspense fallback={<div>Loading posts...</div>}>
        <RecentPosts />
      </Suspense>

      {/* This streams in when ready (doesn't block the page) */}
      <Suspense fallback={<div>Loading analytics...</div>}>
        <AnalyticsChart />
      </Suspense>
    </div>
  );
}

// Result: User sees "Dashboard" + posts immediately
// Analytics chart streams in when the API responds
// No client-side loading spinners needed

Data Fetching Strategies

Sequential Data Fetching

When data requests depend on each other, they must be sequential. Use async/await naturally.

// Sequential: second request depends on first
async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId);           // 1st request
  const posts = await getUserPosts(user.name);  // 2nd (depends on user.name)

  return <div>{user.name}: {posts.length} posts</div>;
}

Parallel Data Fetching

When data requests are independent, fetch them in parallel using Promise.all to avoid request waterfalls.

// Parallel: independent requests — use Promise.all
async function Dashboard() {
  // Start all requests simultaneously
  const [users, posts, analytics] = await Promise.all([
    getUsers(),
    getPosts(),
    getAnalytics(),
  ]);

  return (
    <div>
      <UserList users={users} />
      <PostList posts={posts} />
      <AnalyticsPanel data={analytics} />
    </div>
  );
}

Caching and Revalidation

Next.js extends fetch with caching controls. Understanding the caching layers is essential for RSC performance.

// Next.js fetch caching strategies

// 1. Static (cached indefinitely until revalidated)
const data = await fetch('https://api.example.com/data');
// Equivalent to: { cache: 'force-cache' }

// 2. Revalidate every 60 seconds (ISR)
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 },
});

// 3. Dynamic (no caching — fresh on every request)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
});

// 4. Tag-based revalidation
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});

// In a Server Action:
import { revalidateTag } from 'next/cache';
revalidateTag('posts');  // Purge all fetches tagged 'posts'

Performance Benefits

RSC provides several performance advantages over traditional client-side React:

RSC Performance Benefits:

1. Zero Client JavaScript for Server Components
   - Only Client Components ship JS to the browser
   - Large libraries used on server (markdown, syntax highlighting) add zero bundle

2. Streaming SSR
   - HTML streams to browser as components resolve
   - First Contentful Paint (FCP) is faster
   - Time to Interactive (TTI) is faster (less JS to hydrate)

3. Automatic Code Splitting
   - Each Client Component is automatically code-split
   - No manual dynamic imports needed

4. Reduced Data Transfer
   - Server Components fetch data on the server (same network)
   - No client-side fetch waterfalls
   - No duplicate data in both HTML and JS payload

5. Smaller Hydration Payload
   - Only Client Components need hydration
   - Server Component output is static — no hydration needed

Typical Results:
   Before RSC: 450KB JS bundle, 2.5s TTI
   After RSC:  180KB JS bundle, 1.1s TTI (60% JS reduction)

Common Mistakes

// Mistake 1: Adding "use client" unnecessarily
// BAD: This doesn't need client-side JavaScript
'use client';  // REMOVE THIS
export function UserCard({ name, email }: { name: string; email: string }) {
  return <div>{name} ({email})</div>;  // No hooks, no events = Server Component
}

// Mistake 2: Importing Server Component in Client Component
// BAD: This won't work
'use client';
import { ServerComponent } from './ServerComponent';  // ERROR
// FIX: Pass as children instead
export function ClientWrapper({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>;
}

// Mistake 3: Using "use client" too high in the tree
// BAD: Making a layout a Client Component
'use client';  // This makes EVERYTHING below it client-side
export function Layout({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('dark');
  return <div className={theme}>{children}</div>;
}
// FIX: Extract the interactive part into a small Client Component
// components/ThemeToggle.tsx
'use client';
export function ThemeToggle() {
  const [theme, setTheme] = useState('dark');
  return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}
// layout.tsx — stays as Server Component
export function Layout({ children }: { children: React.ReactNode }) {
  return <div><ThemeToggle />{children}</div>;
}

Frequently Asked Questions

Can I use Server Components without Next.js?

Technically yes, but practically Next.js (App Router) is the primary production-ready framework for RSC. The React team designed RSC to be framework-agnostic, but the bundler integration required is complex. Remix and other frameworks are adding RSC support, but Next.js is the most mature implementation.

Do Server Components replace SSR?

No. Server Components complement SSR. With Next.js App Router, you get both: Server Components render on the server without sending JS to the client, and Client Components are SSR-rendered (for initial HTML) and then hydrated on the client. RSC is a finer-grained approach that lets you choose per-component whether it needs client interactivity.

When should I use "use client"?

Add "use client" when your component needs: useState, useEffect, useRef, or other React hooks; event handlers (onClick, onChange); browser-only APIs (window, localStorage); third-party libraries that use hooks or browser APIs. Keep the "use client" boundary as low in your component tree as possible to maximize Server Components.

Can Server Components use context?

No. React Context (useContext) is a client-side feature. Server Components cannot use or provide context. If you need to share data between Server Components, pass it as props or use a shared data-fetching function. For theming or other global state, wrap your Client Components in a Client Context Provider.

How do Server Components affect SEO?

Positively. Server Components produce HTML on the server, which search engines can crawl. Since Server Components do not require JavaScript to render content, the HTML is available immediately. Combined with streaming, search engines receive content faster than with client-side rendering.

Related Tools and Guides

𝕏 Twitterin LinkedIn
Var dette nyttig?

Hold deg oppdatert

Få ukentlige dev-tips og nye verktøy.

Ingen spam. Avslutt når som helst.

Try These Related Tools

{ }JSON FormatterJSJavaScript Minifier

Related Articles

Next.js App Router: Komplett migreringsguide 2026

Mestr Next.js App Router med denne komplette guiden. Server Components, datahenting, layouter, streaming, Server Actions og trinnvis migrering fra Pages Router.

React Hooks komplett guide: useState, useEffect og Custom Hooks

Mestr React Hooks med praktiske eksempler. useState, useEffect, useContext, useReducer, useMemo, useCallback, custom hooks og React 18+ concurrent hooks.

React Performance: 15 praktiske tips

Optimaliser React-apper med 15 velprøvde teknikker.