2025-05-26 Web Development, Technology, Programming
Server Components vs Server Actions for Data Fetching and Auth in Next.js 15
By O. Wolfson
Next.js 15 introduces stricter rules about where and how you can fetch data, especially when dealing with authentication and cookies. This guide explains the essential concepts and shows you exactly what to do.
The Two Types of Server Code
Think of Next.js 15 as having two distinct environments for server code:
1. Server Components (Read-Only Zone)
- Files:
layout.tsx
,page.tsx
, and other components - Purpose: Display data to users
- What you CAN do: Fetch data from databases, call APIs, read cookies
- What you CANNOT do: Modify cookies, redirect users, change session data
2. Server Actions (Action Zone)
- Files: Functions marked with
"use server"
- Purpose: Handle user interactions and data changes
- What you CAN do: Everything! Read/write cookies, redirect, modify sessions
- When they run: Only when users submit forms or trigger actions
Why This Separation Exists
Server Components need to be predictable and cacheable. If they could modify cookies or redirect users, it would break React's rendering model and cause unpredictable behavior.
The Cookie Problem (And How to Fix It)
The Error You'll See
Error: Cookies can only be modified in a Server Action or Route Handler
Why It Happens
When you use Supabase (or similar auth libraries) in a Server Component like this:
ts// ❌ This will break in layout.tsx or page.tsx
const supabase = await createClient();
const { data: user } = await supabase.auth.getUser(); // Tries to write cookies!
The getUser()
method might refresh tokens and attempt to write new cookies, but Server Components can't do that.
The Solution: Separate Your Clients
For Server Components (reading data):
ts// utils/supabase/server-read.ts
import { cookies } from "next/headers";
import { createServerClient } from "@supabase/ssr";
export async function createReadOnlySupabase() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
// No setAll method = read-only
},
}
);
}
For Server Actions (changing data):
ts// utils/supabase/server-action.ts
import { cookies } from "next/headers";
import { createServerClient } from "@supabase/ssr";
export async function createActionSupabase() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
},
},
}
);
}
Practical Examples
✅ Correct: Reading User Data in a Server Component
ts// app/dashboard/page.tsx
import { createReadOnlySupabase } from '@/utils/supabase/server-read';
export default async function DashboardPage() {
const supabase = await createReadOnlySupabase();
const { data: user } = await supabase.auth.getUser();
if (!user) {
return <div>Please log in</div>;
}
return <div>Welcome, {user.email}!</div>;
}
✅ Correct: Logging Out with a Server Action
ts// app/actions/auth.ts
"use server";
import { createActionSupabase } from "@/utils/supabase/server-action";
import { redirect } from "next/navigation";
export async function logout() {
const supabase = await createActionSupabase();
await supabase.auth.signOut();
redirect("/login");
}
tsx// app/components/LogoutButton.tsx
import { logout } from "@/app/actions/auth";
export function LogoutButton() {
return (
<form action={logout}>
<button type="submit">Sign Out</button>
</form>
);
}
Important Changes in Next.js 15
Cookies Are Now Async
In Next.js 15, you must await the cookies()
function:
ts// ✅ Correct
const cookieStore = await cookies();
// ❌ Wrong (worked in Next.js 14)
const cookieStore = cookies();
Stricter Cookie Rules
The rules about cookie modification are now enforced at runtime:
Operation | Server Components | Server Actions | Route Handlers |
---|---|---|---|
Read cookies | ✅ Yes | ✅ Yes | ✅ Yes |
Write cookies | ❌ No | ✅ Yes | ✅ Yes |
Delete cookies | ❌ No | ✅ Yes | ✅ Yes |
Quick Reference Guide
When building a page that shows data:
- Use Server Components
- Use read-only Supabase client
- Perfect for dashboards, profiles, lists
When handling user actions:
- Use Server Actions
- Use full Supabase client
- Perfect for login, logout, form submissions
When you need full control:
- Use Route Handlers (
app/api/
) - Handle complex logic, file uploads, webhooks
Common Mistakes to Avoid
- Don't call auth methods in Server Components - Use Server Actions instead
- Don't forget to await cookies() - It's async in Next.js 15
- Don't mix read and write operations - Keep them in separate functions
- Don't try to redirect from Server Components - Use Server Actions
Summary
Next.js 15's strict separation keeps your app fast and predictable. Remember:
- Server Components = Read data, show UI
- Server Actions = Handle user interactions, modify data
- Use the right Supabase client for each context
Follow these patterns, and you'll avoid the common errors that trip up many developers when upgrading to Next.js 15.