Technical
React Server Components Changed How I Build
I have been building React apps since 2015. Server Components are the biggest shift in the mental model since hooks arrived. Once I internalized the pattern, half of my usual React boilerplate disappeared. Here is what changed and why it matters.
The Old Mental Model
Every React component shipped to the browser. Data fetching happened in useEffect. Loading states were manual. Every page was a single-page app whether it needed to be or not.
// The pattern every React dev knew
function Posts() {
const [posts, setPosts] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then(r => r.json())
.then(setPosts)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
return <PostList posts={posts} />;
}Four concepts (state, effect, loading, render) for a job that is really just 'show the posts.'
The New Mental Model
Server Components fetch data on the server before any JavaScript runs. They ship HTML, not JS. The same component in the new model:
async function Posts() {
const posts = await fetchPosts();
return <PostList posts={posts} />;
}That is the whole thing. No useState, no useEffect, no loading state. The data arrives with the HTML, and the user sees the posts immediately.
What Stays on the Client
Interactivity. A search bar that filters the list is still a Client Component. A button that triggers a modal is still a Client Component. I mark these with 'use client' at the top of the file.
The rule I follow: start every component as a Server Component. Only add 'use client' when the component genuinely needs browser-only features (state, effects, event handlers).
Why This Matters
Three practical wins:
- Less JavaScript: Server Components do not ship to the browser at all
- Faster first paint: HTML arrives with data, no loading spinner
- Simpler code: no async state management for data fetching
My bundle sizes dropped 40% on the first project where I adopted the pattern. Pages felt instantly faster without any specific optimization.
The Gotchas
Server Components cannot use hooks. They cannot use useState, useEffect, or any browser-only API. This is not a limitation; it is the whole point. If you need those, the component needs to be a Client Component.
The mental shift is learning to ask 'does this component need the browser?' before writing it. Most of the time the answer is no.
Server Actions
The companion feature is Server Actions. Forms call server functions directly, no API route needed:
async function createPost(formData: FormData) {
'use server';
await db.posts.create({ title: formData.get('title') });
revalidatePath('/posts');
}
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button>Create</button>
</form>
);
}No fetch call, no client-side form handler, no optimistic update boilerplate. The mutation runs on the server and the UI refreshes automatically.
The Transition
I did not rewrite old apps. I used Server Components in new code and left legacy Client Components alone. Over time, the Server Component portion grew. No migration project, no rewrite cost.
The Future
This pattern is how new React apps will be built. The split between 'what needs the browser' and 'what doesn't' is fundamental and not going away. Learn it once, apply it forever.
See the React Server Components documentation for the official model and the Next.js App Router docs for the implementation that I use daily.
RELATED READING
The Consulting Shift I Am Making In Year Two
After a year of writing and building, my consulting practice is changing shape. Shorter engagements. Sharper outcomes.
ReadThe Frontend Shift: Shipping Less JavaScript In Year Two
A year ago I reached for Next.js for everything. This year I often reach for nothing.
ReadThe Serverless Lesson I Would Write On A Sticky Note
After a year of shipping serverless projects, one rule explains most of the wins and all of the losses.
Read