I spent the first few months with React Server Components trying to map them onto what I already knew. That was a mistake. The mental model that finally clicked was simpler: most of what I called state was never state.
What state actually was
Look at a typical client-side data component from a few years ago. It has a loading boolean, an error object, the data itself, a refetch function, and usually a stale flag. Five pieces of state, four of which exist purely to describe the progress of a network request.
On the server, that request isn't a lifecycle. It's a value. You await it. The loading state is expressed by where you put a <Suspense> boundary, and the error state by where you put an error.tsx. Both become layout decisions rather than variables you thread through components.
The rule I landed on
Client components are for things that are genuinely about *this browser, right now*: the state of a disclosure, an input someone is typing into, an intersection observer, a media query. Everything else belongs on the server.
That sounds obvious written down. In practice it means aggressively pushing "use client" to the leaves. A page that fetches and renders a list should be a server component that renders one small client component for the interactive bit — not a client component that fetches.
Where it gets awkward
Two things still don't feel solved.
The first is that the server/client boundary is a serialisation boundary, and it's easy to accidentally send a lot across it. A component that takes a whole post object as a prop when it only needs the title has just shipped the body to the browser twice — once in the HTML, once in the RSC payload.
The second is that shared interactive state across sibling server components pushes you toward lifting a client provider higher than you'd like, which quietly re-client-ifies a chunk of the tree. Composition helps — pass server-rendered children *through* the client provider rather than having the provider render them — but you have to be deliberate about it.
Was it worth it
Yes, and not primarily for performance. It's worth it because the resulting code has fewer moving parts. There's no cache to invalidate in the client, no waterfall to trace through three hooks, no skeleton that drifts out of sync with the real layout.
The performance is a side effect of shipping less JavaScript, which was always the point.