client vs server directives
10/24/2024•3 min read
client vs server directives
next.js 13+ splits code between client and server. three key tools help manage this: 'use client', 'use server', and 'server-only'.
'use client'
marks where client-side rendering starts. use when you need:
- browser APIs
- react hooks (useState, useEffect, etc.)
- event handlers
- client-side libraries
"use client";
import { useState } from "react";
export function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
return (
<button onClick={() => setIsDark(!isDark)}>
{isDark ? "light" : "dark"} mode
</button>
);
}
'use server'
marks server functions callable from client. use for:
- form submissions
- data mutations
- server operations
// actions.ts
"use server";
export async function updateUserProfile(formData: FormData) {
const name = formData.get("name");
const email = formData.get("email");
await db.user.update({
where: { email },
data: { name },
});
}
// profile-form.tsx
"use client";
import { updateUserProfile } from "./actions";
export function ProfileForm() {
return (
<form action={updateUserProfile}>
<input name="name" type="text" />
<input name="email" type="email" />
<button type="submit">update</button>
</form>
);
}
'server-only' package
prevents client imports of server code. protects:
- API keys & credentials
- sensitive logic
- server-only operations
install it:
npm install server-only
use it:
// db.ts
import "server-only";
export const db = {
user: {
connection: process.env.DATABASE_URL,
async find(id: string) {
// db query
},
},
};
// trying to import this in client component = build error
combining them
user dashboard with protected data:
// lib/db.ts
import "server-only";
export async function getUserData(userId: string) {
const data = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
return data;
}
// actions.ts
"use server";
import { getUserData } from "../lib/db";
export async function updateUserSettings(userId: string, settings: any) {
const userData = await getUserData(userId);
// update logic
}
// dashboard/page.tsx (server component)
import { getUserData } from "../lib/db";
export default async function DashboardPage() {
const userData = await getUserData("123");
return (
<div>
<UserProfile userData={userData} />
<SettingsForm />
</div>
);
}
// settings-form.tsx
"use client";
import { updateUserSettings } from "../actions";
export function SettingsForm() {
return <form action={updateUserSettings}>{/* fields */}</form>;
}
protecting secrets
// config.ts
import "server-only";
export const config = {
apiKey: process.env.API_KEY,
databaseUrl: process.env.DATABASE_URL,
secretKey: process.env.SECRET_KEY,
};
// actions.ts
"use server";
import "server-only";
import { config } from "./config";
export async function processPayment(amount: number) {
const stripe = new Stripe(config.secretKey);
// process payment
}
proper separation
// lib/auth.ts
import "server-only";
export async function validateUser(token: string) {
// server-only auth logic
}
// api/auth.ts
"use server";
import { validateUser } from "../lib/auth";
export async function handleLogin(formData: FormData) {
const token = formData.get("token");
return validateUser(token);
}
// login.tsx
"use client";
import { handleLogin } from "../api/auth";
export function LoginForm() {
return <form action={handleLogin}>{/* ... */}</form>;
}
error prevention
// ❌ build error
"use client";
import { db } from "./db"; // server-only module in client code
// ✅ correct
"use client";
import { updateUserData } from "./actions"; // server actions ok
function UserForm() {
return <form action={updateUserData}>{/* ... */}</form>;
}
summary
- 'use client': marks client/server boundary
- 'use server': enables server functions from client
- 'server-only': prevents client imports of sensitive code
together they manage the boundary and protect sensitive ops.