React Server Components

React Server Components allow developers to render components on the server, reducing the JavaScript bundle size sent to the client and improving performance. Unlike traditional client-side rendering or server-side rendering, RSCs create a new mental model where some components run exclusively on the server while others run on the client.
The React team's documentation provides the most authoritative explanation of Server Components. They cover the core concepts, architecture, and usage patterns.
Here's a basic example of a Server Component:
// This file runs only on the server
export async function ServerComponent() {
// Direct database access without exposing credentials to the client
const data = await db.query('SELECT * FROM posts')
return (
<div>
<h1>Posts from the Database</h1>
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
Dan Abramov, a member of the React team, has written extensively about the architecture behind React Server Components. His explanations help developers understand how RSCs fit into the React ecosystem.
One key concept he explains is the separation between Server and Client Components:
// This file runs only on the client
'use client'
import { useState } from 'react'
export function ClientComponent() {
// Client components can use React hooks
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
And how they can be composed together:
import { ClientComponent } from './client-component'
export async function ServerComponent() {
const data = await fetchSomeData();
return (
<div>
<h1>Server-rendered content</h1>
<p>{data.message}</p>
{/* Client components can be rendered within server components */}
<ClientComponent />
</div>
)
}
Next.js has embraced React Server Components as a core part of its architecture. Their documentation provides practical examples of using RSCs in a full-stack application context.
Here's how data fetching works in a Next.js Server Component:
// Server Component by default in Next.js 13+
export default async function Page() {
// This data fetching happens on the server
const response = await fetch('https://api.example.com/data')
const data = await response.json()
return (
<main>
<h1>Welcome to my Next.js App</h1>
<div>
{data.items.map(item => (
<article key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</article>
))}
</div>
</main>
)
}
Kent C. Dodds wrote an article about the mental model shift required to use React Server Components. His blog posts help developers understand when to use Server Components and Client Components.
One key pattern he discusses is the "container/presenter" pattern in the RSC context:
// Server Component
import { UserProfilePresenter } from './user-profile-presenter'
export async function UserProfileContainer({ userId }) {
// Fetch data on the server
const userData = await fetchUserData(userId)
const userPosts = await fetchUserPosts(userId)
// Pass data to client component for interactivity
return <UserProfilePresenter userData={userData} posts={userPosts} />
}
// Client Component
'use client'
import { useState } from 'react'
export function UserProfilePresenter({ userData, posts }) {
const [activeTab, setActiveTab] = useState('profile')
return (
<div>
<nav>
<button
onClick={() => setActiveTab('profile')}
className={activeTab === 'profile' ? 'active' : ''}
>
Profile
</button>
<button
onClick={() => setActiveTab('posts')}
className={activeTab === 'posts' ? 'active' : ''}
>
Posts
</button>
</nav>
{activeTab === 'profile' ? (
<div>
<h1>{userData.name}</h1>
<p>{userData.bio}</p>
</div>
) : (
<div>
<h1>Posts by {userData.name}</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)}
</div>
)
}
One of the most powerful features of React Server Components is the ability to stream UI from the server. This allows for progressive rendering of components as data becomes available.

// In a Next.js app
import { Suspense } from 'react'
import Loading from './Loading'
import SlowDataComponent from './SlowDataComponent'
import FastDataComponent from './FastDataComponent'
export default async function Page() {
return (
<div>
<h1>My Dashboard</h1>
{/* Fast component renders immediately */}
<FastDataComponent />
{/* Slow component shows loading state until data is ready */}
<Suspense fallback={<Loading />}>
<SlowDataComponent />
</Suspense>
</div>
)
}
Using RSC, data fetching is moved to the server, which is more efficient here:
// Server Component
export default async function Page({ productId }) {
const product = await getProduct(productId)
const recommendations = await getRecommendations(productId)
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
<h2>You might also like:</h2>
<div className="recommendations">
{recommendations.map(rec => (
<ProductCard key={rec.id} product={rec} />
))}
</div>
</div>
)
}
Start with a server component and enhance it with client components as needed:
// Server Component
import { SubmitButton } from './submit-button'
export function Form() {
return (
<form action="/api/submit" method="post">
<input type="text" name="name" required />
<textarea name="message" required></textarea>
{/* Use a Client Component just for the interactive button */}
<SubmitButton />
</form>
)
}
// Client Component
'use client'
import { useState } from 'react'
export function SubmitButton() {
const [isSubmitting, setIsSubmitting] = useState(false)
return (
<button
type="submit"
disabled={isSubmitting}
onClick={() => setIsSubmitting(true)}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)
}
Since server components cannot access React Hooks, different methods are needed to manage state:
// Server Component
import { UserSettings } from './user-settings'
export async function UserDashboard() {
const user = await getCurrentUser()
const settings = await getUserSettings(user.id)
return (
<div>
<h1>Welcome, {user.name}</h1>
{/* Pass data to client component for state management */}
<UserSettings initialSettings={settings} userId={user.id} />
</div>
)
}
// Client Component
'use client'
import { useState } from 'react'
export function UserSettings({ initialSettings, userId }) {
const [settings, setSettings] = useState(initialSettings)
async function updateSetting(key, value) {
// Update local state immediately for responsive UI
setSettings({ ...settings, [key]: value })
// Send update to server
await fetch(`/api/users/${userId}/settings`, {
method: 'PATCH',
body: JSON.stringify({ [key]: value }),
headers: { 'Content-Type': 'application/json' }
})
}
return (
<div className="settings-panel">
<h2>User Settings</h2>
<div className="setting">
<label>
Dark Mode:
<input
type="checkbox"
checked={settings.darkMode}
onChange={e => updateSetting('darkMode', e.target.checked)}
/>
</label>
</div>
{/* More settings... */}
</div>
)
}
Not all libraries are compatible with server components. Here are some solutions:
// Client Component
'use client'
import { SomeThirdPartyComponent } from 'third-party-library'
export default function ClientWrapper({ data }) {
return <SomeThirdPartyComponent data={data} />
}
// Server Component
import { ClientWrapper } from './client-wrapper'
export async function Page() {
const data = await fetchData()
return (
<div>
<h1>My Page</h1>
{/* Wrap third-party component that requires client features */}
<ClientWrapper data={data} />
</div>
)
}
React Server Components represent a significant shift in how we build React applications. By adopting a hybrid rendering model that leverages both server and client capabilities, developers can create higher-performance and scalable applications.
The resources and code examples provided in this blog should help you understand the core concepts and patterns used in React Server Component development. As this technology continues to mature, we can expect to see more innovative approaches to building modern web applications.