DEV Community

Royce
Royce

Posted on • Originally published at starterpick.com

React Server Components in Boilerplates

TL;DR

React Server Components (RSC) are now the default in Next.js App Router and all modern boilerplates. They eliminate the need for API routes for data fetching, reduce client JavaScript, and simplify auth patterns. The catch: the mental model shift is significant, and mixing server/client components has sharp edges that trip up teams.

What Server Components Actually Do

In the old model (Pages Router), every React component runs on the client:

// Pages Router — runs on client
// Every user downloads this JavaScript
// Every user runs this on their machine
function Dashboard({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('/api/dashboard').then(r => r.json()).then(setData);
  }, []);

  return data ? <DashboardUI data={data} /> : <Loading />;
}
Enter fullscreen mode Exit fullscreen mode

In the App Router with Server Components:

// App Router — runs on server
// This code NEVER ships to the browser
// The browser receives HTML and minimal JS for interactivity

async function Dashboard({ userId }: { userId: string }) {
  // Direct database access — no API layer needed
  const [user, stats, recentActivity] = await Promise.all([
    prisma.user.findUnique({ where: { id: userId } }),
    getDashboardStats(userId),
    getRecentActivity(userId, 10),
  ]);

  return (
    <div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

No useEffect. No loading state. No API route. The data is fetched on the server at request time.


The Server/Client Split

The most important concept: components are server by default, opt-in to client with 'use client'.

// app/dashboard/page.tsx — Server Component (default)
// Runs on server. Can access database, filesystem, env vars.
  const user = await getCurrentUser();
  const data = await getDashboardData(user.id);

  return (
    <main>
      <StaticHeader title="Dashboard" />  {/* Server — no interactivity needed */}
      <DataDisplay data={data} />          {/* Server — just renders data */}
      <InteractiveChart data={data} />     {/* Client — needs useState/animations */}
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode
// components/interactive-chart.tsx — Client Component
'use client';  // <-- This directive marks it as client-side


  const [activePoint, setActivePoint] = useState<number | null>(null);

  // This component IS shipped to the browser
  // It receives `data` as props (serialized from server)
  return (
    <div>
      {activePoint !== null && <Tooltip index={activePoint} data={data} />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Key Rule

Server Components can import Client Components.
Client Components CANNOT import Server Components.

// ✅ Correct: Server imports Client
// server-page.tsx (Server Component)

// ❌ Wrong: Client imports Server
// interactive-button.tsx (Client Component)
Enter fullscreen mode Exit fullscreen mode

How Modern Boilerplates Use Server Components

Authentication Pattern

// app/(dashboard)/layout.tsx — Server Component

  children,
}: {
  children: React.ReactNode;
}) {
  const { userId } = await auth();

  if (!userId) redirect('/sign-in');

  // Fetch user data once here — all child pages inherit it via Context
  const user = await prisma.user.findUnique({ where: { clerkId: userId } });

  return (
    <div className="flex h-screen">
      <main className="flex-1 overflow-auto">{children}</main>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Auth check happens on the server in the layout — no loading flicker, no redirect flash.

Data Co-Location

// Each page fetches exactly what it needs — no over-fetching
// app/(dashboard)/billing/page.tsx
  const { userId } = await auth();
  const subscription = await getSubscription(userId);
  const invoices = await getInvoices(userId, 10);

  return (
    <>
      <ChangePlanButton />  {/* Client Component for interactivity */}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Suspense and Streaming

// Parallel data fetching with independent loading states

  return (
    <div>
      {/* These load independently — slow one doesn't block fast one */}
        <StatsSection />  {/* Slow query — shows skeleton while loading */}
        <ActivityFeed />  {/* Fast query — shows immediately */}
    </div>
  );
}

async function StatsSection() {
  const stats = await getExpensiveStats();  // This can take time
  return <StatsGrid stats={stats} />;
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls in Boilerplates

1. Serialization Errors

Server Components pass data to Client Components as props. Data must be serializable:

// ❌ Not serializable — Date objects become strings, Prisma instances lost
const user = await prisma.user.findUnique({ where: { id } });
return <ClientComponent user={user} />;
// user.createdAt (Date) will be serialized to string

// ✅ Explicit serialization
return <ClientComponent user={JSON.parse(JSON.stringify(user))} />;
// Or better: select only needed fields
const user = await prisma.user.findUnique({
  where: { id },
  select: { id: true, name: true, email: true, plan: true }
});
Enter fullscreen mode Exit fullscreen mode

2. Context in Server Components

// ❌ Can't use Context in Server Components

// ✅ Read from cookies/headers instead
const theme = cookies().get('theme')?.value ?? 'light';
Enter fullscreen mode Exit fullscreen mode

3. Event Handlers Require 'use client'

// ❌ onClick doesn't work in Server Components
  return <button onClick={() => console.log('clicked')}>Click me</button>;
  // Error: Event handlers cannot be passed to Client Component props.
}

// ✅ Extract interactive parts to Client Components
'use client';
  return <button onClick={() => console.log('clicked')}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode

Boilerplate RSC Adoption

Boilerplate RSC Usage App Router
ShipFast ✅ Extensive
Supastarter
Makerkit
T3 Stack ✅ (community) Updating
Epic Stack ❌ Remix (different model) N/A

Find App Router / Server Component boilerplates on StarterPick.

Top comments (0)