Next.js is a robust React framework that supports both server-side and client-side rendering. With the release of Next.js 13, developers gained the ability to use Server and Client Components more efficiently. In this tutorial, you’ll learn how to use both types of components to build fast, scalable applications.
What Are Server and Client Components?
Let’s start with a simple distinction.
- Server Components run only on the server. They don’t ship any JavaScript to the client.
- Client Components run in the browser. They are used when interactivity is required.
This separation helps you optimize your app for performance. You use server components for static or dynamic rendering without client-side overhead. You use client components when you need state, effects, or user events.
Why It Matters
Before Next.js 13, everything in your React app had to be client-side. This often led to large JavaScript bundles. Now, you can split logic cleanly between server and client, which reduces load time and improves performance.
Setting Up Your Next.js Project
To begin, create a Next.js app:
npx create-next-app@latest my-next-app cd my-next-app
Ensure that you enable the App Router by selecting it during the setup process.
Creating Server Components
By default, components in the app/ directory are Server Components. That means unless you mark them otherwise, they won’t ship to the browser.
Example: Display Data from a Database
Create a file: app/products/page.tsx
import { getProducts } from '@/lib/products';
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}Notice that getProducts() is an asynchronous function. This works because Server Components support async/await natively.
Advantages
- Faster page loads
- Smaller bundle sizes
- Access to secure resources (e.g., databases, secrets)
You avoid exposing sensitive logic to the frontend, which is a huge security win.
Creating Client Components
Sometimes, you need interactivity, such as handling a button click. In that case, you need a Client Component.
Example: A Simple Counter
Create a file: components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}Add the counter to your page:
import Counter from '@/components/Counter';
export default function HomePage() {
return (
<div>
<h1>Welcome</h1>
<Counter />
</div>
);
}Key Point
Don’t forget the 'use client'; directive at the top. Without it, React will treat this as a Server Component and throw an error when it encounters useState.
Mixing Server and Client Components
You can combine both types easily. A Server Component can include a Client Component, but not the other way around.
Example: Server Component with Client Interaction
// app/page.tsx
import Counter from '@/components/Counter';
export default function Home() {
return (
<main>
<h1>Dashboard</h1>
<Counter />
</main>
);
}This setup is ideal. You keep most of your code on the server-side and interactive components on the client-side.
Common Use Cases

Best Practices
To get the most out of Server and Client Components, follow these best practices:
1. Minimize Client Components
Client Components add bundle size. Use them only when necessary. Whenever possible, keep logic on the server.
2. Separate Components by Purpose
Don’t mix client and server logic in one file. Keep them modular. For example:
// Server: app/products/page.tsx // Client: components/CartButton.tsx
3. Avoid Passing Functions to Server Components
Server Components can’t handle functions as props from Client Components. Instead, structure your logic carefully and keep interactivity inside Client Components.
4. Use Suspense for Loading States
Suspense works seamlessly with Server Components.
import { Suspense } from 'react';
import ProductList from './ProductList';
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ProductList />
</Suspense>
);
}Suspense provides a smoother user experience by eliminating the need for complex loading logic.
Complete example of a small Next.js website
Folder Structure
/app /page.tsx /components /ProductList.tsx ← Server Component /AddToCartButton.tsx ← Client Component /Cart.tsx ← Client Component /cartStore.ts /lib /products.ts ← Server-side logic (mock DB) /type /cart.ts
1. /lib/products.ts (Server-Side Logic)
// lib/products.ts
export async function getProducts() {
return [
{ id: 1, name: "Laptop", price: 1200 },
{ id: 2, name: "Keyboard", price: 100 },
{ id: 3, name: "Mouse", price: 50 },
];
}2. /components/ProductList.tsx (Server Component)
// components/ProductList.tsx
import AddToCartButton from "./AddToCartButton";
import { getProducts } from "@/lib/products";
export default async function ProductList() {
const products = await getProducts();
return (
<div className="bg-white">
<div className="mx-auto max-w-2xl px-4 py-8 sm:px-6 sm:py-12 lg:max-w-7xl lg:px-8">
<h2 className="text-2xl font-bold tracking-tight text-gray-900 mb-8">
Our Products
</h2>
<div className="space-y-4">
{products.map((p) => (
<div
key={p.id}
className="group relative flex items-center rounded-lg border border-gray-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow duration-200"
>
{/* Product Image */}
<div className="flex-shrink-0 w-16 h-16 overflow-hidden rounded-lg bg-gray-100 mr-4">
<div className="flex h-full w-full items-center justify-center text-gray-400">
<svg
className="h-8 w-8"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.75 7.5h16.5-1.5M4.5 7.5l.75-3h13.5l.75 3-1.5 0"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
/>
</svg>
</div>
</div>
{/* Product Info */}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-indigo-600 transition-colors truncate">
{p.name}
</h3>
<div className="flex items-center mt-1">
<p className="text-xl font-bold text-gray-900 mr-3">
${p.price.toFixed(2)}
</p>
<span className="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800">
In Stock
</span>
</div>
</div>
{/* Add to Cart Button */}
<div className="flex-shrink-0 ml-4">
<AddToCartButton product={p} />
</div>
{/* Hover overlay effect */}
<div className="absolute inset-0 rounded-lg ring-1 ring-black ring-opacity-5 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none" />
</div>
))}
</div>
</div>
</div>
);
}3. /components/AddToCartButton.tsx (Client Component)
// components/AddToCartButton.tsx
"use client";
import { useCartStore } from "./cartStore";
export default function AddToCartButton({ product }: any) {
const addToCart = useCartStore((state) => state.addItem);
return (
<button
className="ml-4 inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => addToCart(product)}
>
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 3h2l.4 2M7 13h10l4-8H5.4m0 0L7 13m0 0l-1.5 1.5M7 13l1.5 1.5M17 21a2 2 0 100-4 2 2 0 000 4zM9 21a2 2 0 100-4 2 2 0 000 4z"
/>
</svg>
Add to Cart
</button>
);
}4. /components/Cart.tsx (Client Component)
// components/Cart.tsx
"use client";
import { CartItem } from "../types/Cart";
import { useCartStore } from "./cartStore";
export default function Cart() {
const items = useCartStore((state) => state.items);
return (
<div>
<h2>Your Cart</h2>
{items.length === 0 && <p>No items added.</p>}
<ul>
{items.map((item: CartItem) => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
</div>
);
}5. /components/cartStore.ts (Client State Management)
// components/cartStore.ts
import { create } from "zustand";
import { CartState, CartItem } from "../types/Cart";
export const useCartStore = create<CartState>((set, get) => ({
items: [],
addItem: (item: CartItem) =>
set((state) => ({
items: [...state.items, item],
})),
removeItem: (id: string | number) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
clearCart: () => set({ items: [] }),
getTotalPrice: () => get().items.reduce((sum, item) => sum + item.price, 0),
getTotalItems: () => get().items.length,
}));6. /app/page.tsx (App Entry Point – Server Component)
// app/page.tsx
import ProductList from "@/components/ProductList";
import Cart from "@/components/Cart";
export default function HomePage() {
return (
<main className="min-h-screen bg-gray-50">
{/* Hero Section */}
<section className="bg-white border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl md:text-6xl">
<span className="block">Welcome to</span>
<span className="block text-indigo-600">Our Store</span>
</h1>
<p className="mt-3 max-w-md mx-auto text-base text-gray-500 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl">
Discover amazing products and enjoy a seamless shopping
experience.
</p>
</div>
</div>
</section>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="lg:grid lg:grid-cols-3 lg:gap-8">
{/* Product List */}
<section className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Products
</h2>
<ProductList />
</div>
</section>
{/* Cart Sidebar */}
<aside className="mt-8 lg:mt-0">
<div className="sticky top-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Shopping Cart
</h2>
<Cart />
</div>
</div>
</aside>
</div>
</div>
</main>
);
}7. /types/cart.ts
// types/cart.ts
export interface CartItem {
id: string | number;
name: string;
price: number;
quantity?: number;
}
export interface CartState {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string | number) => void;
clearCart: () => void;
getTotalPrice: () => number;
getTotalItems: () => number;
}8. Install zustand
npm i zustand
What This Website Demonstrates

Ready to Run
- Make sure you’ve created a Next.js 13+ project using the App Router.
- Replace files as shown above.
- Run the project:
npm run dev
Visit http://localhost:3000 and enjoy a fully working demo with both Server and Client Components.

When you click the add to cart button, the item will be added to the shopping cart section.

Debugging Tips
Running into issues? Here are a few things to check:
- Make sure you’re using
'use client'only when needed. - Avoid using React hooks within server components.
- Use
console.log()inside the server for debug messages; it will log in your terminal, not the browser.
Finally
Next.js gives you the best of both worlds — powerful server-side rendering and dynamic client-side interactivity. By understanding when and how to use Server and Client Components, you can build faster, more scalable web apps.
Use Server Components for heavy lifting. Keep your interactive logic inside Client Components. Follow the best practices outlined here, and your app will be lean, fast, and developer-friendly.
This article was originally published on Medium.



