React Server Components Cheat Sheet

Server Components Basics

Component Types

// Server Component (default in App Router)
async function ServerComponent() {
  const data = await fetchData();
  return <div>{data.title}</div>;
}

// Client Component (requires 'use client')
'use client';
function ClientComponent() {
  const [state, setState] = useState();
  return <button onClick={() => setState('clicked')}>Click</button>;
}

// Mixed Component Tree
function ParentComponent() {
  return (
    <div>
      <ServerComponent />
      <ClientComponent />
    </div>
  );
}

File Structure

// app/page.tsx - Server Component (default)
export default async function Page() {
  return <ServerComponent />;
}

// app/components/ClientButton.tsx - Client Component
'use client';
export default function ClientButton() {
  return <button>Click me</button>;
}

// app/components/ServerData.tsx - Server Component
export default async function ServerData() {
  const data = await fetch('/api/data');
  return <div>{data.title}</div>;
}

Data Fetching

Server-Side Data Fetching

// Direct database access
async function UserProfile({ userId }) {
  const user = await db.users.findUnique({
    where: { id: userId }
  });
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// API calls from server
async function ProductList() {
  const products = await fetch('https://api.example.com/products', {
    cache: 'force-cache' // Default caching
  }).then(res => res.json());
  
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

Caching Strategies

// Force cache (default)
const data = await fetch('/api/data');

// Revalidate every 60 seconds
const data = await fetch('/api/data', {
  next: { revalidate: 60 }
});

// No cache
const data = await fetch('/api/data', {
  cache: 'no-store'
});

// Revalidate on demand
const data = await fetch('/api/data', {
  next: { tags: ['products'] }
});

// Revalidate specific tag
revalidateTag('products');

Parallel Data Fetching

async function Dashboard() {
  // Fetch data in parallel
  const [users, posts, analytics] = await Promise.all([
    fetch('/api/users'),
    fetch('/api/posts'),
    fetch('/api/analytics')
  ]);
  
  const [usersData, postsData, analyticsData] = await Promise.all([
    users.json(),
    posts.json(),
    analytics.json()
  ]);
  
  return (
    <div>
      <UserList users={usersData} />
      <PostList posts={postsData} />
      <Analytics data={analyticsData} />
    </div>
  );
}

Props and Serialization

Serializable Props

// Server Component with serializable props
async function ProductCard({ productId, category }) {
  const product = await getProduct(productId);
  
  return (
    <div>
      <h2>{product.name}</h2>
      <p>{product.description}</p>
      <ClientButton productId={productId} /> {/* Pass serializable data */}
    </div>
  );
}

// Client Component receiving props
'use client';
function ClientButton({ productId }) {
  const handleClick = () => {
    // productId is serializable
    addToCart(productId);
  };
  
  return <button onClick={handleClick}>Add to Cart</button>;
}

Non-Serializable Props

// Server Component - avoid passing functions
async function ServerComponent() {
  const data = await fetchData();
  
  // Wrong - function not serializable
  // return <ClientComponent onAction={() => handleAction()} />;
  
  // Correct - pass data, handle logic in client
  return <ClientComponent data={data} />;
}

// Client Component handles the logic
'use client';
function ClientComponent({ data }) {
  const handleAction = () => {
    // Handle action logic here
  };
  
  return (
    <div>
      <p>{data.title}</p>
      <button onClick={handleAction}>Action</button>
    </div>
  );
}

Component Patterns

Server-Client Boundary

// Server Component with client boundary
async function ProductPage({ productId }) {
  const product = await getProduct(productId);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      
      {/* Client boundary for interactive features */}
      <ClientProductActions product={product} />
    </div>
  );
}

// Client Component for interactivity
'use client';
function ClientProductActions({ product }) {
  const [quantity, setQuantity] = useState(1);
  
  return (
    <div>
      <input
        type="number"
        value={quantity}
        onChange={(e) => setQuantity(parseInt(e.target.value))}
      />
      <button onClick={() => addToCart(product.id, quantity)}>
        Add to Cart
      </button>
    </div>
  );
}

Conditional Rendering

// Server-side conditional rendering
async function ConditionalComponent({ userId }) {
  const user = await getUser(userId);
  
  if (!user) {
    return <div>User not found</div>;
  }
  
  if (user.role === 'admin') {
    return <AdminDashboard user={user} />;
  }
  
  return <UserDashboard user={user} />;
}

Error Boundaries

// Server Component error handling
async function SafeDataFetching() {
  try {
    const data = await fetch('/api/unreliable-endpoint');
    return <DataDisplay data={data} />;
  } catch (error) {
    return <ErrorFallback error={error.message} />;
  }
}

// Client error boundary
'use client';
function ClientErrorBoundary({ children }) {
  return (
    <ErrorBoundary fallback={<div>Something went wrong</div>}>
      {children}
    </ErrorBoundary>
  );
}

Layout and Styling

Server Component Layouts

// Server Component layout
async function ProductLayout({ children }) {
  const categories = await getCategories();
  
  return (
    <div className="product-layout">
      <aside>
        <CategoryNav categories={categories} />
      </aside>
      <main>
        {children}
      </main>
    </div>
  );
}

// Usage in page
export default async function ProductPage() {
  return (
    <ProductLayout>
      <ProductList />
    </ProductLayout>
  );
}

CSS-in-JS Considerations

// Server Component - use CSS modules or global styles
import styles from './ServerComponent.module.css';

async function ServerComponent() {
  return (
    <div className={styles.container}>
      <h1 className={styles.title}>Server Rendered</h1>
    </div>
  );
}

// Client Component - can use CSS-in-JS
'use client';
import styled from 'styled-components';

const StyledButton = styled.button`
  background: blue;
  color: white;
`;

function ClientComponent() {
  return <StyledButton>Styled Button</StyledButton>;
}

Performance Optimization

Streaming and Suspense

// Streaming with Suspense
async function StreamingPage() {
  return (
    <div>
      <Suspense fallback={<div>Loading header...</div>}>
        <Header />
      </Suspense>
      
      <Suspense fallback={<div>Loading content...</div>}>
        <SlowContent />
      </Suspense>
      
      <Suspense fallback={<div>Loading footer...</div>}>
        <Footer />
      </Suspense>
    </div>
  );
}

// Slow server component
async function SlowContent() {
  await new Promise(resolve => setTimeout(resolve, 2000));
  const data = await fetch('/api/slow-data');
  
  return <div>{data.content}</div>;
}

Preloading Data

// Preload data for better performance
async function ProductList() {
  // Preload data
  const productsPromise = getProducts();
  
  return (
    <div>
      <h1>Products</h1>
      <Suspense fallback={<ProductSkeleton />}>
        <ProductGrid productsPromise={productsPromise} />
      </Suspense>
    </div>
  );
}

async function ProductGrid({ productsPromise }) {
  const products = await productsPromise;
  
  return (
    <div className="grid">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

State Management

Server State vs Client State

// Server Component - server state
async function UserProfile({ userId }) {
  const user = await getUser(userId);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      {/* Pass server state to client component */}
      <ClientUserActions user={user} />
    </div>
  );
}

// Client Component - client state
'use client';
function ClientUserActions({ user }) {
  const [isEditing, setIsEditing] = useState(false);
  const [localName, setLocalName] = useState(user.name);
  
  return (
    <div>
      {isEditing ? (
        <input
          value={localName}
          onChange={(e) => setLocalName(e.target.value)}
        />
      ) : (
        <p>{localName}</p>
      )}
      <button onClick={() => setIsEditing(!isEditing)}>
        {isEditing ? 'Save' : 'Edit'}
      </button>
    </div>
  );
}

Server Actions

// Server Action
async function createPost(formData) {
  'use server';
  
  const title = formData.get('title');
  const content = formData.get('content');
  
  const post = await db.posts.create({
    data: { title, content }
  });
  
  revalidatePath('/posts');
  redirect('/posts');
}

// Client Component using server action
'use client';
import { createPost } from './actions';

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

Best Practices

Component Design

// Good - Keep server components focused on data
async function ProductPage({ productId }) {
  const product = await getProduct(productId);
  
  return (
    <div>
      <ProductHeader product={product} />
      <ProductDetails product={product} />
      <ClientProductActions product={product} />
    </div>
  );
}

// Good - Extract client boundaries
'use client';
function ClientProductActions({ product }) {
  const [quantity, setQuantity] = useState(1);
  
  return (
    <div>
      <QuantitySelector value={quantity} onChange={setQuantity} />
      <AddToCartButton productId={product.id} quantity={quantity} />
    </div>
  );
}

Error Handling

// Server Component error handling
async function SafeServerComponent() {
  try {
    const data = await fetch('/api/data');
    return <DataDisplay data={data} />;
  } catch (error) {
    // Log error on server
    console.error('Server error:', error);
    return <ErrorFallback />;
  }
}

// Client error boundary for client components
'use client';
function ClientErrorBoundary({ children }) {
  return (
    <ErrorBoundary
      fallback={<div>Something went wrong in client</div>}
      onError={(error) => {
        // Log client errors
        console.error('Client error:', error);
      }}
    >
      {children}
    </ErrorBoundary>
  );
}

Performance Tips

// Use React.cache for expensive operations
import { cache } from 'react';

const getExpensiveData = cache(async (id) => {
  // This will be cached for the same id
  return await expensiveOperation(id);
});

async function CachedComponent({ id }) {
  const data = await getExpensiveData(id);
  return <div>{data.result}</div>;
}

// Avoid client-server waterfalls
async function OptimizedPage() {
  // Fetch all data in parallel
  const [users, posts, settings] = await Promise.all([
    getUsers(),
    getPosts(),
    getSettings()
  ]);
  
  return (
    <div>
      <UserList users={users} />
      <PostList posts={posts} />
      <SettingsPanel settings={settings} />
    </div>
  );
}

Security Considerations

// Server Component - validate input
async function UserProfile({ userId }) {
  // Validate userId on server
  if (!isValidUserId(userId)) {
    throw new Error('Invalid user ID');
  }
  
  const user = await getUser(userId);
  
  // Sanitize data before sending to client
  const safeUser = {
    id: user.id,
    name: user.name,
    email: user.email
    // Don't send sensitive data like password hash
  };
  
  return <ClientUserProfile user={safeUser} />;
}

// Client Component - handle user input safely
'use client';
function ClientUserProfile({ user }) {
  const handleUpdate = async (formData) => {
    // Validate on client as well
    const name = formData.get('name');
    if (!name || name.length < 2) {
      alert('Name must be at least 2 characters');
      return;
    }
    
    // Send to server action
    await updateUser(formData);
  };
  
  return (
    <form action={handleUpdate}>
      <input name="name" defaultValue={user.name} />
      <button type="submit">Update</button>
    </form>
  );
}