Skip to main content

React 2026: Server Components and Next.js 15

Created: February 23, 2026 Larry Qu 4 min read

Introduction

React has fundamentally shifted toward server-first architecture. With Server Components now stable and Next.js 15, building performant React applications has never been easier. This guide covers the modern React paradigm.


What Are Server Components?

The Concept

Server Components (RSC) run exclusively on the server:

  • Zero JavaScript sent to client
  • Direct database access
  • Native Node.js modules
  • Automatic code splitting

Client vs Server

// Server Component (default in App Router)
async function ServerComponent() {
  // This runs on the server only
  const user = await db.users.findById(params.id);
  
  return (
    <div>
      <h1>{user.name}</h1>
      {/* No JS bundle for this component */}
    </div>
  );
}

// Client Component
'use client';

function ClientComponent() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count}
    </button>
  );
}

Next.js 15 Features

1. Partial Prerendering

// app/blog/[slug]/page.tsx
import { Suspense } from 'react';

// Static content loads instantly
async function BlogContent({ slug }: { slug: string }) {
  const post = await db.posts.findBySlug(slug);
  return <article>{post.content}</article>;
}

// Dynamic content streams in
async function Comments({ slug }: { slug: string }) {
  const comments = await db.comments.findByPost(slug);
  return <CommentsList data={comments} />;
}

export default function Page({ params }: { params: { slug: string } }) {
  return (
    <div>
      {/* Static - served instantly */}
      <BlogContent slug={params.slug} />
      
      {/* Dynamic - streams in */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments slug={params.slug} />
      </Suspense>
    </div>
  );
}

2. Server Actions

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  
  await db.posts.create({ title });
  
  // Revalidate the blog page
  revalidatePath('/blog');
  
  return { success: true };
}

// app/page.tsx
import { createPost } from './actions';

export default function Page() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" />
      <button type="submit">Create</button>
    </form>
  );
}

3. Enhanced Fetch

// Automatic caching and revalidation
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { 
      revalidate: 3600, // Revalidate every hour
      tags: ['my-data'] // Tag for on-demand revalidation
    }
  });
  
  return res.json();
}

// In API route - trigger revalidation
import { revalidateTag } from 'next/cache';

export async function POST() {
  revalidateTag('my-data');
  return Response.json({ revalidated: true });
}

Data Fetching Patterns

Parallel Requests

// Fetch in parallel - no waterfall
async function Page() {
  const [user, posts, comments] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json()),
  ]);
  
  return (
    <div>
      <UserProfile user={user} />
      <PostsList posts={posts} />
      <CommentsList comments={comments} />
    </div>
  );
}

Streaming with Suspense

import { Suspense } from 'react';

function Profile() {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <ProfileContent />
    </Suspense>
  );
}

function Dashboard() {
  return (
    <div className="grid">
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      
      <Suspense fallback={<ActivitySkeleton />}>
        <ActivityFeed />
      </Suspense>
    </div>
  );
}

Migration Guide

From Pages to App Router

// Old: pages/api/users.ts
export default function handler(req, res) {
  const users = await db.users.findAll();
  res.status(200).json(users);
}

// New: app/api/users/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const users = await db.users.findAll();
  return NextResponse.json(users);
}

Using Client Libraries

// Server Component
async function ServerPage() {
  // Direct database access
  const data = await db.query('SELECT * FROM users');
  
  // Pass to client component
  return <ClientComponent data={data} />;
}

// Client Component with hooks
'use client';

function ClientComponent({ data }) {
  const [query, setQuery] = useState('');
  
  // Use SWR for client-side fetching
  const { data: filtered } = useSWR(
    `/api/users?q=${query}`,
    fetcher
  );
  
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <List data={filtered || data} />
    </div>
  );
}

Best Practices

1. Keep Server Components Default

// ✅ Good - Server Component
async function PostList() {
  const posts = await db.posts.findAll();
  return <List items={posts} />;
}

// Only add 'client' when needed
'use client';
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
  // Interactive logic here
}

2. Use Server Actions

// ✅ Good - Server Actions for mutations
export async function deletePost(id: string) {
  await db.posts.delete(id);
  revalidatePath('/posts');
}

// For complex client state, use mutations
'use client';
function SearchInput() {
  const [query, setQuery] = useState('');
  
  return (
    <input 
      value={query}
      onChange={e => setQuery(e.target.value)}
    />
  );
}

3. Cache Wisely

// Dynamic data - no cache
fetch('https://api/user', { cache: 'no-store' });

// Static data - cache indefinitely  
fetch('https://api/config');

// Revalidate on demand
fetch('https://api/posts', { next: { tags: ['posts'] } });

External Resources

Documentation

Tools


Key Takeaways

  • Server Components run on server, zero JS to client
  • Next.js 15 brings partial prerendering
  • Server Actions handle mutations without API routes
  • Suspense enables streaming and progressive loading
  • Keep server default, use ‘client’ sparingly
  • Parallel fetching prevents waterfalls

Resources

Comments

Share this article

Scan to read on mobile