client vs server directives

10/24/2024/3 min read

title: client vs server directives description: understanding 'use client', 'use server', and 'server-only'. managing the boundary. date: 2024-10-24 tags: [react, nextjs]

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
code.jsx
Jsx
"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
code.jsx
Jsx
// 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:

code.bash
Bash
npm install server-only

use it:

code.jsx
Jsx
// 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:

code.jsx
Jsx
// 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

code.jsx
Jsx
// 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

code.jsx
Jsx
// 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

code.jsx
Jsx
// ❌ 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.