Server Components vs. Client Components: A Practical Solo Dev Rulebook
Part of Building This Site and Small Business Websites
By Paul Peery · August 15, 2026 · 4 min read

You do not need to turn an entire page into a Client Component just because one button needs an onClick handler.
When React Server Components first landed in Next.js, a lot of developers took the easy way out: slapping 'use client' at the top of every file whenever a build error popped up. Doing that gets your app running, but it tosses away the real benefits of the modern Next.js App Router—direct database queries without API routes, smaller client bundles, and faster initial page renders.
As a solo builder, your goal is simple code that stays maintainable. Here is the exact boundary I draw between Server Components and Client Components in production.
Default to the server until the browser demands an event
The default rule is simple: everything is a Server Component unless it needs browser APIs or interactive React hooks.
Server Components execute entirely on your server (or during build time). They never send their component JavaScript to the visitor's browser. That means you can safely run direct database queries, read server environment variables, or import heavy parsing libraries without bloating what your visitors download.
You only need 'use client' when a component does one of three things:
- Uses state or lifecycle hooks (
useState,useEffect,useReducer). - Listens for user interactions (
onClick,onChange,onSubmit). - Accesses browser-only APIs (
window,localStorage,navigator).
If a component only takes data as props and renders HTML or CSS, keep it on the server. Even a deeply nested layout or sidebar can stay a Server Component as long as it does not attach client event listeners.
Push client boundaries down to the leaves of your tree
The biggest mistake I see in App Router projects is putting 'use client' at the top-level page file. The moment you mark a page component as a Client Component, every component it imports also runs on the client.
Instead, push the client boundary down to the smallest interactive "leaf" component possible.
Suppose you have a blog post page with a heavy markdown renderer, a database query for post content, and a tiny "Copy Link" button at the bottom. If you add 'use client' to the page file, your visitor has to download the code for the entire markdown engine just to copy a link.
Instead, keep the page on the server. Fetch your post data directly in the page component, render the markdown on the server, and extract just the button into its own small CopyButton.tsx file marked with 'use client'. The server renders the layout and text, and only a tiny snippet of JavaScript ships for the button.
If you want to cut down client script weight even further, removing unnecessary JavaScript libraries in favor of native CSS and server rendering is usually the fastest win.
Handling forms without breaking server architecture
Forms used to require local state for every single text field. In modern Next.js, that is no longer true.
If your form is simple—like an email newsletter signup or a basic contact form—you can use React Server Actions directly inside a Server Component. The <form action={myServerAction}> syntax submits native FormData to your server function without needing 'use client' or local state.
You only need to make the form a Client Component if you require instant client-side validation, rich autocomplete dropdowns, or optimistic UI updates while the form submits. When you do need client-side form management, pass the Server Action to the Client Component as a prop. That keeps your database logic on the server while keeping your form UI responsive.
Remember to treat incoming form data with the same caution you would any public endpoint. When I laid out the security setup for a solo site, validating all form input on the server side—regardless of client validation—was the non-negotiable line.
The third-party widget trap
Many popular npm packages were built for older React architectures and do not include the 'use client' directive in their own source files. When you import them into a Server Component, Next.js throws an error because the library tries to read window or call React.createContext on the server.
You do not need to make your whole page a Client Component to fix this. Create a small wrapper file:
// components/DatePickerWrapper.tsx
'use client';
import { DatePicker } from 'third-party-date-library';
export default DatePicker;
Now you can import DatePickerWrapper into any Server Component. The wrapper defines the client boundary without converting the surrounding layout into client code.
The honest trade-off: mental overhead
The real downside of the Server Component model is the mental shift it requires. In classic React, every component behaved the exact same way. You could pass functions freely down the tree and sprinkle useState wherever you wanted.
With Server and Client Components, you have to think about serialization boundaries. You cannot pass a server-only function (like an unexported database callback) or a non-serializable object as a prop to a Client Component. When you refactor, you will occasionally run into serialization errors that feel frustrating compared to an old single-page app.
For a solo developer, that extra discipline pays for itself in cleaner separation: data fetching stays grouped with the layout, and client code is isolated to where user interaction actually happens.
The 10-second decision checklist
When building a new component, run through this quick filter:
- Does it fetch data from a database or secret API? Keep it a Server Component.
- Is it a static wrapper, header, or text layout? Keep it a Server Component.
- Does it use
useState,useEffect, or custom hooks? Make it a Client Component with'use client'. - Does it need
onClick,onChange, or browser APIs? Isolate just that specific interactive element into a Client Component leaf.
Treat Server Components as your default, isolate client interactions to the smallest buttons or inputs you can, and your app will stay fast and clean.
Keep reading
All postsComments
No comments yet — be the first!
