Back to Blog
ReactServer ComponentsNext.jsPerformance

Understanding React Server Components

Master React Server Components. From basics to data fetching to composition patterns.

B
Bootspring Team
Engineering
July 15, 2021
6 min read

React Server Components render on the server with zero client JavaScript. Here's how to use them effectively.

Server vs Client Components#

1// Server Component (default in Next.js App Router) 2// app/page.tsx 3async function Page() { 4 // Can use async/await directly 5 const data = await fetchData(); 6 7 // Can access server resources 8 const config = process.env.API_KEY; 9 10 return <div>{data.title}</div>; 11} 12 13// Client Component 14// components/Counter.tsx 15'use client'; 16 17import { useState } from 'react'; 18 19function Counter() { 20 const [count, setCount] = useState(0); 21 22 return ( 23 <button onClick={() => setCount(c => c + 1)}> 24 Count: {count} 25 </button> 26 ); 27}

When to Use Each#

Server Components: ✓ Fetching data ✓ Accessing backend resources ✓ Keeping sensitive data on server ✓ Large dependencies ✓ Static content Client Components: ✓ Interactivity (onClick, onChange) ✓ useState, useEffect, useContext ✓ Browser APIs ✓ Custom hooks with state ✓ Event listeners

Data Fetching#

1// Server Component with data fetching 2async function UserProfile({ userId }: { userId: string }) { 3 const user = await prisma.user.findUnique({ 4 where: { id: userId }, 5 }); 6 7 if (!user) { 8 notFound(); 9 } 10 11 return ( 12 <div> 13 <h1>{user.name}</h1> 14 <p>{user.email}</p> 15 </div> 16 ); 17} 18 19// Parallel data fetching 20async function Dashboard() { 21 // These run in parallel 22 const userPromise = getUser(); 23 const postsPromise = getPosts(); 24 const analyticsPromise = getAnalytics(); 25 26 const [user, posts, analytics] = await Promise.all([ 27 userPromise, 28 postsPromise, 29 analyticsPromise, 30 ]); 31 32 return ( 33 <div> 34 <UserCard user={user} /> 35 <PostList posts={posts} /> 36 <AnalyticsChart data={analytics} /> 37 </div> 38 ); 39} 40 41// Sequential when needed 42async function UserWithPosts({ userId }: { userId: string }) { 43 const user = await getUser(userId); 44 const posts = await getPostsByAuthor(user.id); 45 46 return ( 47 <div> 48 <h1>{user.name}</h1> 49 <PostList posts={posts} /> 50 </div> 51 ); 52}

Composition Patterns#

1// Server Component wrapping Client Component 2// app/page.tsx (Server) 3import { ClientSearch } from './ClientSearch'; 4 5async function SearchPage() { 6 const categories = await getCategories(); 7 8 return ( 9 <div> 10 <h1>Search</h1> 11 {/* Pass server data to client component */} 12 <ClientSearch categories={categories} /> 13 </div> 14 ); 15} 16 17// components/ClientSearch.tsx (Client) 18'use client'; 19 20import { useState } from 'react'; 21 22export function ClientSearch({ categories }: { categories: string[] }) { 23 const [query, setQuery] = useState(''); 24 25 return ( 26 <div> 27 <input 28 value={query} 29 onChange={(e) => setQuery(e.target.value)} 30 /> 31 <select> 32 {categories.map(cat => ( 33 <option key={cat}>{cat}</option> 34 ))} 35 </select> 36 </div> 37 ); 38} 39 40// Children pattern - Server components as children 41// layout.tsx (Server) 42import { Sidebar } from './Sidebar'; 43 44export default function Layout({ children }) { 45 return ( 46 <div className="flex"> 47 <Sidebar /> {/* Server Component */} 48 <main>{children}</main> 49 </div> 50 ); 51} 52 53// ClientWrapper.tsx (Client) 54'use client'; 55 56export function ClientWrapper({ children }) { 57 const [isOpen, setIsOpen] = useState(true); 58 59 return ( 60 <div className={isOpen ? 'expanded' : 'collapsed'}> 61 {children} {/* Server Components work here */} 62 </div> 63 ); 64}

Streaming and Suspense#

1import { Suspense } from 'react'; 2 3// Streaming with Suspense 4async function Page() { 5 return ( 6 <div> 7 {/* Renders immediately */} 8 <Header /> 9 10 {/* Streams when ready */} 11 <Suspense fallback={<PostsSkeleton />}> 12 <Posts /> 13 </Suspense> 14 15 {/* Streams independently */} 16 <Suspense fallback={<CommentsSkeleton />}> 17 <Comments /> 18 </Suspense> 19 </div> 20 ); 21} 22 23// Slow component streams later 24async function Posts() { 25 const posts = await fetchPosts(); // 2 seconds 26 27 return ( 28 <ul> 29 {posts.map(post => ( 30 <li key={post.id}>{post.title}</li> 31 ))} 32 </ul> 33 ); 34} 35 36// Loading UI 37// app/posts/loading.tsx 38export default function Loading() { 39 return <PostsSkeleton />; 40}

Caching#

1// Next.js caching 2async function getPosts() { 3 const res = await fetch('https://api.example.com/posts', { 4 cache: 'force-cache', // Default - cached 5 // cache: 'no-store', // Never cache 6 // next: { revalidate: 3600 }, // Revalidate after 1 hour 7 }); 8 9 return res.json(); 10} 11 12// Revalidation 13import { revalidatePath, revalidateTag } from 'next/cache'; 14 15// Tag-based revalidation 16async function getPosts() { 17 const res = await fetch('https://api.example.com/posts', { 18 next: { tags: ['posts'] }, 19 }); 20 return res.json(); 21} 22 23// Server Action to revalidate 24async function createPost(formData: FormData) { 25 'use server'; 26 27 await db.post.create({ data: { ... } }); 28 revalidateTag('posts'); 29} 30 31// Path-based revalidation 32async function updatePost(id: string) { 33 'use server'; 34 35 await db.post.update({ ... }); 36 revalidatePath('/posts'); 37}

Server Actions#

1// Server Action in Server Component 2async function TodoList() { 3 const todos = await getTodos(); 4 5 async function addTodo(formData: FormData) { 6 'use server'; 7 8 const title = formData.get('title') as string; 9 await db.todo.create({ data: { title } }); 10 revalidatePath('/todos'); 11 } 12 13 return ( 14 <div> 15 <form action={addTodo}> 16 <input name="title" /> 17 <button type="submit">Add</button> 18 </form> 19 <ul> 20 {todos.map(todo => ( 21 <li key={todo.id}>{todo.title}</li> 22 ))} 23 </ul> 24 </div> 25 ); 26} 27 28// Server Action in Client Component 29// actions.ts 30'use server'; 31 32export async function addTodo(formData: FormData) { 33 const title = formData.get('title') as string; 34 await db.todo.create({ data: { title } }); 35 revalidatePath('/todos'); 36} 37 38// components/AddTodoForm.tsx 39'use client'; 40 41import { addTodo } from './actions'; 42import { useFormStatus } from 'react-dom'; 43 44function SubmitButton() { 45 const { pending } = useFormStatus(); 46 return <button disabled={pending}>Add</button>; 47} 48 49export function AddTodoForm() { 50 return ( 51 <form action={addTodo}> 52 <input name="title" /> 53 <SubmitButton /> 54 </form> 55 ); 56}

Error Handling#

1// error.tsx - Error boundary 2'use client'; 3 4export default function Error({ 5 error, 6 reset, 7}: { 8 error: Error; 9 reset: () => void; 10}) { 11 return ( 12 <div> 13 <h2>Something went wrong!</h2> 14 <button onClick={() => reset()}>Try again</button> 15 </div> 16 ); 17} 18 19// not-found.tsx 20export default function NotFound() { 21 return ( 22 <div> 23 <h2>Not Found</h2> 24 <p>Could not find requested resource</p> 25 </div> 26 ); 27} 28 29// Throwing not found 30import { notFound } from 'next/navigation'; 31 32async function Page({ params }: { params: { id: string } }) { 33 const post = await getPost(params.id); 34 35 if (!post) { 36 notFound(); 37 } 38 39 return <div>{post.title}</div>; 40}

Best Practices#

Architecture: ✓ Default to Server Components ✓ Add 'use client' only when needed ✓ Keep client components small ✓ Lift data fetching to Server Components Performance: ✓ Fetch data in parallel ✓ Use Suspense for streaming ✓ Cache appropriately ✓ Preload critical data Patterns: ✓ Pass data as props to Client Components ✓ Use children for composition ✓ Co-locate Server Actions ✓ Handle errors at boundaries

Conclusion#

React Server Components enable efficient server-side rendering with zero client JavaScript for static content. Use them for data fetching and heavy computations, while reserving Client Components for interactivity. Combine with Suspense for streaming and Server Actions for mutations.

Share this article

Help spread the word about Bootspring