All Insights
notes· 7 min read

React Server Components Mental Model

How I finally understood RSC

SV
Sri VardhanJanuary 30, 2024
Share on Twitter
Share on LinkedIn
Copy link

The mental model that made React Server Components click for me after months of confusion.

I bounced off React Server Components three times before they clicked. The documentation describes the mechanism, but the mental model takes longer to settle. Here is how I finally got it to stick.

Stop thinking about server vs client

The phrase "server components" makes you think the boundary is about where code runs. That is true mechanically, but it is the wrong frame for design decisions. The real boundary is about what changes over time.

  • Server components render once per request. They are functions of inputs (params, search params, cookies, fetched data) and output a description of UI. They never re-render in response to user interaction.
  • Client components render on the client and can re-render. They hold state, run effects, attach event handlers, and respond to interaction.

Once I framed RSC as "static for this request, then interactive islands inside" everything else fell into place.

The composition rule

Server components can render client components. Client components cannot render server components, but they can receive them as children or props. That is the rule that confuses people the most, and it has a clean explanation.

A server component runs on the server and serializes its output. That output can include placeholders for client components, because we know what to do with those: hydrate them. But once you are inside a client component, you are in a tree that runs on the client. The server cannot reach back in. So you pass the server rendered nodes in as props or children before the boundary, not after.

In practice, this means the most flexible pattern is:

  • Make your layout a server component.
  • Pass interactive parts as children to client wrappers.
  • Keep useState, useEffect, and event handlers inside the smallest possible client subtrees.

"use client" is a boundary, not a tag

use client does not make a component "the client component." It marks the boundary below which everything is client. Every import from inside that file ends up in the client bundle.

This is why putting use client at the top of your root layout defeats the purpose. You have just declared that the entire app is a client component tree. The wins of RSC come from keeping that boundary low in the tree, near the leaves that actually need interactivity.

Data fetching is just function calls

In a server component, fetching data is a normal async function call. No useEffect, no SWR, no React Query. You write:

tsx
async function ProductPage({ id }) {
 const product = await getProduct(id)
 return <ProductView product={product} />
}

The data fetching, the rendering, and the response are one continuous server side flow. This is the part that genuinely simplifies code. I used to need three layers (route handler, client query hook, render) to do what RSC does in one function.

Client side state libraries (React Query, SWR, Zustand) still earn their keep when the data changes after the initial render. Just do not reach for them when the page is loading data once and rendering it.

The serialization trap

Anything that crosses the server-to-client boundary must be serializable. That means no functions as props from server to client components, no class instances, no Dates inside complex objects unless you are careful. The compiler will warn you, but the lesson is to keep the boundary props simple: primitives, plain objects, and React nodes.

If you find yourself wanting to pass a function from a server component to a client component, you almost always want a server action instead.

Server actions are the other half

Server actions complete the picture. They let a client component invoke server code without you writing an API route. They run on the server, can mutate data, and return values that React knows how to integrate with the client tree.

Together, server components handle the read path and server actions handle the write path. Once both clicked for me, I stopped reaching for client side fetching libraries on most pages.

The shortest version

  • Server components are static for this request.
  • Client components are interactive after hydration.
  • The tree flows from server down into client, never the other way.
  • use client is a boundary, place it as low as possible.
  • Fetch data in server components. Mutate with server actions.

If you are building on Next.js and want help structuring a real codebase around this model, I write more about it in my notes on Next.js architecture and frontend patterns.

References

Tagged

#react#rsc#mental-models
SV

Sri Vardhan

Independent technology studio of one. I help founders and small teams ship serious software without the consultancy overhead. More about me.

Want to discuss this topic?

I am always happy to dig deeper. If a piece sparked an idea or a disagreement, send it over. I read every message myself.

Get in Touch