client vs server directives

10/24/20243 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.