Server Components (SSR)
Overview
Server Components are the default in Next.js App Router. They run on the server and fetch data during the request, providing Server-Side Rendering (SSR).
Characteristics
- ✅ Run on server only - No JavaScript sent to client
- ✅ Direct database access - Can use server-side Supabase client
- ✅ Async by default - Can use
async/awaitdirectly - ✅ No client-side interactivity - Cannot use hooks, event handlers, or browser APIs
Implementation Pattern
// app/[primaryTeam]/(team-layout)/page.tsx
import { getCampaigns } from "@/services/campaign/getCampaigns";
export default async function Page({
params,
}: {
params: { primaryTeam: string };
}) {
// Direct async data fetching in Server Component
const campaigns = await getCampaigns(params.primaryTeam);
// Conditional redirect based on data
if (campaigns.data?.length === 1) {
redirect(`/${params.primaryTeam}/campaign/${campaigns.data[0].id}`);
}
return (
<div>
{/* Render with fetched data */}
<CampaignList campaigns={campaigns.data} />
</div>
);
}
Common Server Component Patterns
1. Layout Data Fetching
// app/[primaryTeam]/(team-layout)/layout.tsx
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: { primaryTeam: string };
}) {
// Fetch data for layout (sidebar, navbar)
const userData = await getAuthUser();
const campaigns = await getCampaigns(params.primaryTeam);
const teams = await getUserTeams();
return (
<SidebarProvider>
<AppSidebar
user={userData}
teams={teams?.data?.teams}
campaigns={campaigns.data}
/>
{children}
</SidebarProvider>
);
}
2. Page Data Fetching with Error Handling
// app/[primaryTeam]/campaign/[slug]/page.tsx
export default async function Page({
params
}: {
params: { slug: string }
}) {
const campaign = await getCampaignBySlug(params.slug);
// Handle errors
if (!campaign.data || campaign.error) {
return <CampaignNotFound />;
}
return <CampaignStats campaignData={campaign.data} />;
}
3. Parallel Data Fetching
// Multiple data sources fetched in parallel
export default async function Page({ params }) {
// These fetch in parallel, not sequentially
const [campaign, teams, permissions] = await Promise.all([
getCampaignBySlug(params.slug),
getUserTeams(),
acessCheck({ teamId: params.primaryTeam }),
]);
return <Dashboard campaign={campaign} teams={teams} />;
}
Service Layer Pattern
Server Components use the Service Layer (src/services/) for data access:
// services/campaign/getCampaigns.ts
"use server";
import { createClient } from "@/lib/supabase/server";
import { QueryResponse } from "@/types/common";
export const getCampaigns = async (
teamID: string
): Promise<QueryResponse> => {
const supabase = createClient(); // Server-side client
const { data: { user } } = await supabase.auth.getUser();
// Complex query logic
const { data, error } = await supabase
.from("campaigns")
.select("status,id,campaignName,jurisdictionName")
.eq("campaign_user_roles_team.user", user?.id);
return {
error: false,
data,
};
};
Service Layer Benefits:
- ✅ Reusable data access logic
- ✅ Centralized error handling
- ✅ Type-safe return values (
QueryResponse) - ✅ Business logic encapsulation
Where SSR is Used
-
All Page Components (
app/**/page.tsx)- Initial page load with data
- SEO-critical content
-
Layout Components (
app/**/layout.tsx)- Shared data (user, teams, campaigns)
- Navigation data
-
Data Pre-fetching
- Fetching data before rendering
- Passing data to Client Components as props