When Static Pages Weren’t Enough, Client Components Took Over - Pt 5
Series: Part 5 of Rendering with Intent
Breaking down how I learned to choose rendering strategies intentionally rather than relying on defaults.
Most of WonderBook’s interface could run on the server or be cached. Some parts needed to stay awake. They had to listen, respond, and update in real time.
That job belonged to Client Components.
When interaction matters more than static speed
Next.js 15 made one thing clear: you don’t mark something "use client" unless it needs it. Once you do, you’re opting into hydration, JavaScript, and reactivity.
For WonderBook, this trade-off was worth it in only a few places:
- Live Editors, Chapters, titles, and covers update instantly while autosaving in the background.
- Realtime Likes, On the Explore page, likes appear instantly through Supabase Realtime.
- Interactive Forms, Sign-up and payment flows rely on validation, focus states, and animation.
Everything else could stay server-rendered.
A simple principle guided the structure:
If it moves, it belongs to the client. If it stays still, render it once and serve it fast.
A page with both worlds
Client Components worked best when paired with a Server Component that prepared their data. The server fetched, validated, and structured. The client reacted and displayed.
// app/dashboard/page.tsx export const revalidate = 120 // 2 minutes export default async function DashboardPage() { const user = await getCurrentUser() const projects = await getRecentProjects(user.id) return <DashboardClient initialProjects={projects} userId={user.id} /> }
// app/dashboard/_components/dashboard-client.tsx 'use client' import { useEffect, useState } from 'react' export default function DashboardClient({ initialProjects, userId }) { const [projects, setProjects] = useState(initialProjects) useEffect(() => { const interval = setInterval(async () => { const response = await fetch(`/api/projects?user=${userId}`) const data = await response.json() setProjects(data) }, 10000) return () => clearInterval(interval) }, [userId]) return ( <div className="grid gap-4"> {projects.map(project => ( <div key={project.id} className="p-4 border rounded"> <h3>{project.name}</h3> <p>{project.status}</p> </div> ))} </div> ) }
Server Components handled data. Client Components handled interaction. The pattern kept pages fast and users engaged without pushing everything into the browser.
Keeping real-time under control
Adding reactivity everywhere can cause chaos. To prevent that, every Client Component followed three rules:
- Receive, don’t fetch. The server prepared data first. The client only listened for changes.
- Render optimistically. When a user liked a story, the UI updated right away and synced later.
- Scope carefully. Only the parts that truly needed interaction were marked "use client".
This kept WonderBook responsive without heavy bundles or race conditions.
The human layer
When users typed in the story editor, words appeared instantly. When they tapped like, the heart filled before the database confirmed it.
The experience felt immediate and personal. It reacted the way people expect technology to respond when it’s paying attention.
Client Components made that possible. They didn’t replace the server; they complemented it.
What I learned
- Start from the server. Use static or server rendering until you find a reason not to.
- Add interactivity with purpose. Every "use client" should serve a visible experience, not convenience.
- Keep boundaries clean. Server Components prepare; Client Components react.
- Test the feel. Interaction isn’t about data accuracy, it’s about rhythm and timing.
Client Components gave WonderBook its sense of presence. They turned a static story builder into something responsive and alive.
Rendering strategy isn’t just about speed. It’s about how close the product feels to the user.
In case you missed the other posts, here they are:
The Rendering with Intent Series
- Part 1: Rendering Blindly Cost Me Speed, Money, and Sanity. Here’s How I Fixed It
- Part 2: Static Rendering. Doing the Work Before Anyone Asks For It
- Part 3: I Wanted Static Speed Without Serving Yesterday’s Data. ISR Was the Answer
- Part 4: Switched to SSR When “Good Enough” Started Causing Real Problems
- Part 5: When Static Pages Weren’t Enough, Client Components Took Over [You are here]
- Part 6: I Made Data Feel Faster Without Actually Speeding It Up