React Server Components in Practice
React Server Components represent the biggest architectural shift in React since hooks. And like hooks, the discourse has generated more confusion than clarity. Half the blog posts say RSC will change everything. The other half say they’re overhyped. Having shipped server components in production, I can tell you: they’re neither. They’re a tool that changes specific things significantly and leaves other things alone. Here’s what I’ve learned from actually using them.The Mental Model Shift
The single most important concept to internalize: React components now run in two places. Some run on the server. Some run on the client. The boundary between them is explicit, and understanding where that boundary sits is the entire game. Before RSC, the mental model was simple. All React runs in the browser. Data comes from APIs. Components render HTML from that data. The server sends a JavaScript bundle, the browser executes it, React hydrates, and the page is interactive. With RSC, the model is different:'use client' at the top. This inverts the old model where everything was client-side unless you explicitly did SSR.
When to Use Server vs Client Components
This is the practical question everyone asks first. Here’s my decision framework:Use server components when
- The component fetches data — server components can query databases directly
- The component renders static or semi-static content — blog posts, product listings, dashboards
- The component uses large dependencies that don’t need to be in the client bundle — markdown parsers, syntax highlighters, date formatting libraries
- The component accesses server-only resources — environment variables, file system, internal APIs
Use client components when
- The component needs interactivity — clicks, inputs, form state, hover effects
- The component uses React state or effects —
useState,useEffect,useRef - The component uses browser APIs —
window,localStorage,IntersectionObserver - The component needs real-time updates — WebSocket connections, polling
The gray area
Many components have both static and interactive parts. A product card might have a static image, title, and price (server) but an “Add to Cart” button (client). The pattern for this is composition:Data Fetching Patterns
RSC fundamentally changes data fetching. In the old model, you fetch data inuseEffect or with a library like React Query. With RSC, server components fetch data directly — no hooks, no loading states, no waterfalls.
Direct database access
useEffect. No loading state management. No React Query cache configuration. The data is fetched on the server, rendered to HTML, and sent to the client. The Promise.all parallelizes the queries — no waterfall.
Parallel data fetching with Suspense
For more granular loading states, use Suspense boundaries:When you still need client-side fetching
RSC doesn’t replace all client-side data fetching. You still need client-side fetching for:- Real-time data — WebSocket updates, polling for new messages
- User-initiated fetches — Search-as-you-type, infinite scroll
- Optimistic updates — Show the result before the server confirms
- Data that changes based on client state — Filtered lists where the filter is client-side
Streaming and Suspense
Streaming is RSC’s superpower for perceived performance. Instead of waiting for all data before sending any HTML, the server sends HTML as components resolve.How it works
- Server starts rendering the page
- When a component is wrapped in
<Suspense>, the server sends the fallback immediately - When the component’s data resolves, the server streams the real content
- The browser swaps the fallback for the real content — no full page reload
Nested Suspense boundaries
You can nest Suspense boundaries for progressive loading:user loads. Once the user resolves, the inner Suspense boundaries take over for the individual sections. This avoids the “blank page” problem while still streaming content progressively.
Composition Patterns
The server/client boundary creates new composition patterns that take some getting used to.Pattern: Server component wrapping client component
This is the most common pattern. The server component fetches data and passes it as props to a client component:Pattern: Client component accepting server component children
Client components can render server components passed aschildren. This is crucial for layouts:
NavigationMenu is a server component, but it’s rendered through a client component’s children prop. This works because the server component is pre-rendered on the server and passed as a serialized tree.
Pattern: Passing server data via context
You can’t use context providers in server components, but you can wrap a client context provider around server components:Common Mistakes
Having migrated a production app to RSC, here are the mistakes I encountered:1. Making too many components client components
The most common mistake is reflexively adding'use client' because a child component needs interactivity. Instead, extract the interactive part into its own client component and keep the parent as a server component.
2. Passing non-serializable props across the boundary
Server components pass props to client components via serialization. This means you can’t pass functions, class instances, or other non-serializable values:3. Importing server-only code in client components
If a client component imports a module that uses server-only APIs (database, file system), it will fail at build time. Use theserver-only package to get clear errors:
db, they’ll get a clear build error instead of a confusing runtime crash.
4. Forgetting about caching
Server components run on every request by default in Next.js (dynamic rendering). For pages that don’t need real-time data, opt into static rendering or add caching:Performance Implications
RSC has significant performance implications — mostly positive, but with nuances.What improves
Smaller client bundles. Server component code never reaches the browser. Dependencies used only in server components (database clients, markdown parsers, syntax highlighters) are completely excluded from the client bundle. I’ve seen bundle sizes drop 30-50% after migrating data-heavy pages to RSC. No client-side data fetching waterfalls. In a traditional SPA, the sequence is: download JS → execute JS → render → fetch data → re-render. With RSC: server fetches data → renders HTML → streams to client. The data is already embedded in the HTML. Better LCP. HTML arrives faster because the server can start sending content before all data is ready (streaming). The browser doesn’t need to download and execute JavaScript before showing content.What to watch
Server response time. If your server is slow (cold starts, slow database queries), RSC won’t help. You’re moving the latency from the client to the server, not eliminating it. Optimize your server and database first. Streaming overhead. Each Suspense boundary adds a small amount of overhead to the response. Don’t create 50 Suspense boundaries on a single page. Server load. Every page view now does server-side work. If you were previously serving a static SPA from a CDN, RSC means more server compute. Plan for this in your infrastructure.Migration Strategy From SPA
If you’re migrating an existing SPA to RSC, don’t rewrite everything at once. Here’s the incremental approach I used:Phase 1: App Router adoption (2-4 weeks)
Move to Next.js App Router with'use client' at the top of every page. Everything still runs on the client — you’re just adopting the new router.
Phase 2: Layout server components (1-2 weeks)
Convert layouts (nav, sidebar, footer) to server components. These are usually static and easy to convert.Phase 3: Data-heavy pages (4-8 weeks)
One page at a time, move data fetching from client-side hooks to server components. Start with the simplest pages (static content, simple queries) and work toward complex ones.Phase 4: Optimize boundaries (2-4 weeks)
Review the server/client boundary placement. Push'use client' boundaries deeper. Extract interactive bits into smaller client components.
Don’t aim for 100% server components. Interactive applications will always have significant client-side code. The goal is to put the right code in the right place — data fetching and static rendering on the server, interactivity on the client.
My Take
After shipping RSC in production, here’s my honest assessment: RSC is the right direction for React. The mental model of “server components for data, client components for interaction” maps well to how most web applications actually work. The performance benefits are real — smaller bundles, no fetch waterfalls, streaming HTML. But the migration cost is significant. The'use client' boundary requires thinking about every component differently. Libraries need to be RSC-compatible. The debugging story is more complex (is this error from the server or the client?).
For new projects: use RSC from the start. The patterns feel natural when you’re not fighting existing code.
For existing SPAs: migrate incrementally, page by page, prioritizing data-heavy pages where the performance gains are most significant.
For purely interactive applications (real-time editors, canvas-based tools, complex forms): RSC offers less benefit. The bulk of your code is legitimately client-side. Don’t force it.
The future of React is hybrid — server where it makes sense, client where it must be. RSC gives us the primitives to make that split explicit and deliberate. That’s a genuine step forward.