React & Next.js · Frontend
Server or Client Component? A practical guide for the Next.js App Router
When a component needs "use client", how to keep the client boundary small, and the children pattern that lets server content live inside client wrappers.
In the Next.js App Router, every component is a Server Component unless you opt out with the "use client" directive. Server Components render on the server and send HTML — no JavaScript for that component reaches the browser. Client Components are hydrated in the browser so they can be interactive.
Deciding between them comes down to one question: does this component need the browser?
When you need "use client"
Add the directive when a component uses any of these:
- State and lifecycle hooks:
useState,useReducer,useEffect. - Event handlers such as
onClickoronChange. - Browser-only APIs:
window,localStorage,IntersectionObserver. - Libraries that rely on any of the above, such as animation or chart libraries.
Everything else — layout, text, fetching data, reading from a database or CMS — can stay on the server.
Push the client boundary down
"use client" marks a boundary: the component and everything it imports become part of the client bundle. Putting it at the top of a page ships the whole page as JavaScript. Instead, keep the page on the server and make only the interactive leaf a Client Component.
// app/blog/[slug]/page.tsx — Server Component
import { LikeButton } from './LikeButton'
export default async function Post({ params }) {
const post = await getPost((await params).slug) // runs on the server
return (
<article>
<h1>{post.title}</h1>
<div>{post.body}</div>
<LikeButton postId={post.id} />
</article>
)
}// app/blog/[slug]/LikeButton.tsx — Client Component
'use client'
import { useState } from 'react'
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false)
return (
<button aria-pressed={liked} onClick={() => setLiked(!liked)}>
{liked ? 'Liked' : 'Like'}
</button>
)
}Only the button ships JavaScript; the article stays as plain HTML.
Props crossing the boundary must be serialisable
Data passed from a Server Component to a Client Component is serialised. Strings, numbers, booleans, plain objects, arrays and dates are fine. Functions and class instances are not — so pass an ID, not a database record with methods attached.
The children pattern
A Client Component can’t import a Server Component, but it can render one passed in as children. This is how you wrap server-rendered content in client behaviour, like an animated container or a modal.
'use client'
import { useState, type ReactNode } from 'react'
export function Collapsible({ title, children }: { title: string; children: ReactNode }) {
const [open, setOpen] = useState(false)
return (
<section>
<button aria-expanded={open} onClick={() => setOpen(!open)}>{title}</button>
{open && children}
</section>
)
}The page can now render <Collapsible title="Details"><ServerRenderedDetails /></Collapsible> — the details are still rendered on the server.
A quick checklist
- Start every component as a Server Component.
- Add
"use client"only when it needs state, effects, events or browser APIs. - Move the directive to the smallest interactive piece.
- Pass serialisable data across the boundary.
- Use
childrento put server content inside client wrappers.
Related articles
- React & Next.js
Writing React components that stay reusable
Practical rules for components that survive their second and third use: intent-based props, composition with children, and forwarding native props.
Read article - CSS & Design, Frontend
From Figma to responsive UI: a practical handoff checklist
The steps I follow to turn a Figma file into a responsive, accessible interface — from extracting tokens to the states designs usually forget.
Read article
Contact
Have a project or a role in mind? Let’s talk.
Send a message through the contact form, or email me directly.