Skip to main content

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/await directly
  • 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

  1. All Page Components (app/**/page.tsx)

    • Initial page load with data
    • SEO-critical content
  2. Layout Components (app/**/layout.tsx)

    • Shared data (user, teams, campaigns)
    • Navigation data
  3. Data Pre-fetching

    • Fetching data before rendering
    • Passing data to Client Components as props