# tutorials · react
React Server Components 2026: explained with runnable Next.js 15 code.
React Server Components (RSC) render on the server, ship zero client JavaScript, and stream to the browser. This tutorial explains what they actually are, when to use them, when to reach for `use client`, and how streaming and the `use()` hook fit together in a real Next.js 15 App Router project.
What RSC actually are
A React Server Component is a component that runs on the server, produces HTML, and never ships JavaScript to the browser. It can read from a database directly, call an API with a private key, or import a heavy library (like markdown parsers or PDF generators) without adding a byte to the client bundle.
The trade-off: server components cannot use hooks (useState, useEffect), cannot attach event handlers, and cannot access window or document. For that, you drop a 'use client' boundary.
A server component: fetch and render
In Next.js 15 App Router, every component in app/ is a server component by default. This one fetches posts from a database directly and renders them.
// app/posts/page.tsx
import { db } from '@/lib/db';
export default async function PostsPage() {
const posts = await db.query.posts.findMany({
orderBy: (p, { desc }) => desc(p.publishedAt),
limit: 10,
});
return (
<ul>
{posts.map((p) => (
<li key={p.id}>
<a href={`/posts/${p.slug}`}>{p.title}</a>
</li>
))}
</ul>
);
}No API route, no useEffect, no loading spinner. The database driver runs on the server; the browser only sees the rendered HTML. Client JavaScript for this component: 0 bytes.
A client component: interactivity
When you need useState, onClick, or browser APIs, add 'use client' at the top of the file.
// components/like-button.tsx
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [likes, setLikes] = useState(0);
return (
<button
onClick={() => {
setLikes((n) => n + 1);
fetch('/api/like', { method: 'POST', body: JSON.stringify({ postId }) });
}}
>
Like ({likes})
</button>
);
}Then compose the client component inside a server component. The server component sends the client-component payload to the browser; the browser hydrates only that leaf, not the whole tree.
// app/posts/[slug]/page.tsx (server component)
import { db } from '@/lib/db';
import { LikeButton } from '@/components/like-button';
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await db.query.posts.findFirst({
where: (p, { eq }) => eq(p.slug, slug),
});
if (!post) return <p>Not found.</p>;
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<LikeButton postId={post.id} />
</article>
);
}
Streaming with Suspense
React 19 lets you stream chunks of a server-rendered page as they finish, instead of waiting for the slowest fetch. Wrap the slow part in <Suspense> and give it a fallback.
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { UserStats } from './user-stats';
import { RecentOrders } from './recent-orders';
export default function Dashboard() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading stats</p>}>
<UserStats />
</Suspense>
<Suspense fallback={<p>Loading orders</p>}>
<RecentOrders />
</Suspense>
</>
);
}The <h1> and the two fallbacks stream to the browser immediately. Each <Suspense> child streams its real content as its data resolves. Time-to-first-byte drops from “slowest fetch” to “instant”.
The use() hook: read promises inside client components
The use() hook (stable in React 19) reads a promise and unwraps its resolved value. In a client component, you can pass a promise down from a server component and use use() to consume it.
// app/orders/page.tsx (server)
import { OrderList } from './order-list';
import { fetchOrders } from '@/lib/orders';
export default function OrdersPage() {
const ordersPromise = fetchOrders(); // No await, pass the promise
return (
<div>
<h1>Orders</h1>
<OrderList ordersPromise={ordersPromise} />
</div>
);
}
// components/order-list.tsx (client)
'use client';
import { use } from 'react';
import type { Order } from '@/lib/types';
export function OrderList({ ordersPromise }: { ordersPromise: Promise<Order[]> }) {
const orders = use(ordersPromise);
return (
<ul>
{orders.map((o) => (
<li key={o.id}>{o.total} on {o.createdAt}</li>
))}
</ul>
);
}The server component starts the fetch and streams the shell. The client component reads the promise as soon as it arrives, wrapped in <Suspense> for the fallback. No useEffect, no loading state management.
When to reach for a client component
- Interactive UI: buttons, modals, tabs, sliders.
- Anything that uses
useState,useEffect,useRef. - Browser APIs:
window,localStorage,IntersectionObserver,fetchfrom the client. - Third-party client libraries: charts, editors, maps, drag-and-drop.
When a server component is the right answer
- Reading from a database or private API.
- Rendering markdown, PDFs, or heavy transforms on the server.
- SEO-critical content that must be in the initial HTML.
- Anywhere you can move code off the client bundle without breaking interactivity.
