Back to Blog
ReactServer ComponentsRSCPerformance

React Server Components Introduction

Learn React Server Components for server-rendered components that reduce client JavaScript bundles.

B
Bootspring Team
Engineering
January 23, 2019
6 min read

React Server Components (RSC) render on the server, reducing client JavaScript while enabling direct database access. Here's how they work.

Server vs Client Components#

1// Server Component (default in App Router) 2// app/page.js 3async function HomePage() { 4 // Can use async/await directly 5 const data = await fetchFromDatabase(); 6 7 return ( 8 <main> 9 <h1>Welcome</h1> 10 <DataDisplay data={data} /> 11 </main> 12 ); 13} 14 15// Client Component (needs 'use client' directive) 16// components/Counter.js 17'use client'; 18 19import { useState } from 'react'; 20 21function Counter() { 22 const [count, setCount] = useState(0); 23 24 return ( 25 <button onClick={() => setCount(count + 1)}> 26 Count: {count} 27 </button> 28 ); 29}

Data Fetching#

1// Server Component with data fetching 2// app/users/page.js 3async function UsersPage() { 4 // Fetch directly - no useEffect needed 5 const users = await prisma.user.findMany(); 6 7 return ( 8 <div> 9 <h1>Users</h1> 10 <ul> 11 {users.map(user => ( 12 <li key={user.id}>{user.name}</li> 13 ))} 14 </ul> 15 </div> 16 ); 17} 18 19// With error handling 20async function SafeDataFetch() { 21 try { 22 const data = await fetchData(); 23 return <DataView data={data} />; 24 } catch (error) { 25 return <ErrorMessage error={error} />; 26 } 27}

Mixing Server and Client#

1// Server Component 2// app/dashboard/page.js 3import { ClientChart } from './ClientChart'; 4 5async function Dashboard() { 6 // Fetch on server 7 const stats = await getStats(); 8 9 return ( 10 <div> 11 <h1>Dashboard</h1> 12 13 {/* Pass data to client component */} 14 <ClientChart data={stats.chartData} /> 15 16 {/* Server-rendered static content */} 17 <StatsSummary stats={stats} /> 18 </div> 19 ); 20} 21 22// Client Component 23// app/dashboard/ClientChart.js 24'use client'; 25 26import { useEffect, useRef } from 'react'; 27import Chart from 'chart.js'; 28 29export function ClientChart({ data }) { 30 const canvasRef = useRef(null); 31 32 useEffect(() => { 33 const chart = new Chart(canvasRef.current, { 34 type: 'line', 35 data: data, 36 }); 37 38 return () => chart.destroy(); 39 }, [data]); 40 41 return <canvas ref={canvasRef} />; 42}

Composition Patterns#

1// Server Component wrapping Client Component 2// app/posts/page.js 3import { InteractiveList } from './InteractiveList'; 4 5async function PostsPage() { 6 const posts = await getPosts(); 7 8 return ( 9 <div> 10 <h1>Posts</h1> 11 12 {/* Server data passed to client interactivity */} 13 <InteractiveList 14 items={posts} 15 renderItem={(post) => ( 16 <PostCard key={post.id} post={post} /> 17 )} 18 /> 19 </div> 20 ); 21} 22 23// Client Component for interactivity 24'use client'; 25 26export function InteractiveList({ items, renderItem }) { 27 const [filter, setFilter] = useState(''); 28 29 const filtered = items.filter(item => 30 item.title.toLowerCase().includes(filter.toLowerCase()) 31 ); 32 33 return ( 34 <div> 35 <input 36 value={filter} 37 onChange={e => setFilter(e.target.value)} 38 placeholder="Filter..." 39 /> 40 <ul> 41 {filtered.map(renderItem)} 42 </ul> 43 </div> 44 ); 45}

Streaming with Suspense#

1// app/page.js 2import { Suspense } from 'react'; 3 4async function SlowComponent() { 5 const data = await slowFetch(); // Takes 3 seconds 6 return <div>{data}</div>; 7} 8 9export default function Page() { 10 return ( 11 <div> 12 <h1>Dashboard</h1> 13 14 {/* Fast content renders immediately */} 15 <QuickStats /> 16 17 {/* Slow content streams in */} 18 <Suspense fallback={<Loading />}> 19 <SlowComponent /> 20 </Suspense> 21 22 {/* Multiple suspense boundaries */} 23 <Suspense fallback={<ChartSkeleton />}> 24 <AsyncChart /> 25 </Suspense> 26 </div> 27 ); 28}

Server Actions#

1// Server Action in Server Component 2// app/posts/page.js 3async function PostsPage() { 4 async function createPost(formData) { 5 'use server'; 6 7 const title = formData.get('title'); 8 const content = formData.get('content'); 9 10 await prisma.post.create({ 11 data: { title, content }, 12 }); 13 14 revalidatePath('/posts'); 15 } 16 17 return ( 18 <form action={createPost}> 19 <input name="title" placeholder="Title" /> 20 <textarea name="content" placeholder="Content" /> 21 <button type="submit">Create Post</button> 22 </form> 23 ); 24} 25 26// Server Action with Client Component 27// actions.js 28'use server'; 29 30export async function submitForm(formData) { 31 const data = Object.fromEntries(formData); 32 await saveToDatabase(data); 33 revalidatePath('/'); 34} 35 36// components/Form.js 37'use client'; 38 39import { submitForm } from '../actions'; 40import { useFormStatus } from 'react-dom'; 41 42function SubmitButton() { 43 const { pending } = useFormStatus(); 44 return ( 45 <button disabled={pending}> 46 {pending ? 'Submitting...' : 'Submit'} 47 </button> 48 ); 49} 50 51export function ContactForm() { 52 return ( 53 <form action={submitForm}> 54 <input name="email" type="email" required /> 55 <textarea name="message" required /> 56 <SubmitButton /> 57 </form> 58 ); 59}

Caching and Revalidation#

1// Cached fetch (default) 2async function CachedData() { 3 const data = await fetch('https://api.example.com/data'); 4 // Cached indefinitely by default 5 return <div>{data}</div>; 6} 7 8// Revalidate after time 9async function TimedRevalidation() { 10 const data = await fetch('https://api.example.com/data', { 11 next: { revalidate: 3600 }, // Revalidate every hour 12 }); 13 return <div>{data}</div>; 14} 15 16// No caching 17async function FreshData() { 18 const data = await fetch('https://api.example.com/data', { 19 cache: 'no-store', 20 }); 21 return <div>{data}</div>; 22} 23 24// Manual revalidation 25import { revalidatePath, revalidateTag } from 'next/cache'; 26 27async function updatePost(id, data) { 28 'use server'; 29 30 await prisma.post.update({ where: { id }, data }); 31 32 revalidatePath('/posts'); 33 revalidateTag('posts'); 34}

Database Access#

1// Direct database access in Server Components 2// app/users/[id]/page.js 3import { prisma } from '@/lib/prisma'; 4import { notFound } from 'next/navigation'; 5 6async function UserPage({ params }) { 7 const user = await prisma.user.findUnique({ 8 where: { id: params.id }, 9 include: { 10 posts: true, 11 profile: true, 12 }, 13 }); 14 15 if (!user) { 16 notFound(); 17 } 18 19 return ( 20 <div> 21 <h1>{user.name}</h1> 22 <p>{user.email}</p> 23 24 <h2>Posts</h2> 25 <ul> 26 {user.posts.map(post => ( 27 <li key={post.id}>{post.title}</li> 28 ))} 29 </ul> 30 </div> 31 ); 32}

Loading States#

1// app/posts/loading.js 2export default function Loading() { 3 return ( 4 <div className="loading"> 5 <div className="skeleton" /> 6 <div className="skeleton" /> 7 <div className="skeleton" /> 8 </div> 9 ); 10} 11 12// app/posts/page.js 13async function PostsPage() { 14 const posts = await getPosts(); // loading.js shows during this 15 16 return ( 17 <ul> 18 {posts.map(post => ( 19 <li key={post.id}>{post.title}</li> 20 ))} 21 </ul> 22 ); 23}

Error Handling#

1// app/posts/error.js 2'use client'; 3 4export default function Error({ error, reset }) { 5 return ( 6 <div className="error"> 7 <h2>Something went wrong!</h2> 8 <p>{error.message}</p> 9 <button onClick={() => reset()}>Try again</button> 10 </div> 11 ); 12} 13 14// app/posts/page.js 15async function PostsPage() { 16 const posts = await getPosts(); // If this throws, error.js renders 17 18 return <PostList posts={posts} />; 19}

Best Practices#

Server Components: ✓ Use for static content ✓ Direct data fetching ✓ Database queries ✓ Reduce client JS Client Components: ✓ Event handlers (onClick, etc.) ✓ Browser APIs ✓ State (useState, useReducer) ✓ Effects (useEffect) Patterns: ✓ Fetch data at the top ✓ Pass to client components as props ✓ Use Suspense for streaming ✓ Server Actions for mutations Avoid: ✗ 'use client' on everything ✗ Fetching in client components ✗ Large client bundles ✗ Waterfall requests

Conclusion#

React Server Components reduce client JavaScript by rendering on the server with direct database access. Use them for data fetching and static content, while reserving Client Components for interactivity. Combine with Suspense for streaming, Server Actions for mutations, and proper caching for optimal performance. The key is choosing the right component type for each use case.

Share this article

Help spread the word about Bootspring