React Server Components spent a few years as an experimental pattern we prototyped cautiously, usually on a single low-stakes feature where we could afford to be wrong about the approach without much consequence. This year, for any new feature in a React app, starting with a server component and only reaching for a client component when interactivity genuinely requires it has become the unremarkable default, the thing a developer does without stopping to think about it rather than a deliberate architectural choice made fresh each time.
The mental shift that once felt disruptive, deciding explicitly what needs to run on the client versus the server, now happens without much conscious thought for most features. It's a good sign, we think, when a once-novel pattern stops being interesting enough to write about at length, and this post is honestly a bit of an exception to how little we now discuss the distinction day to day.
What made the difference wasn't any single framework improvement, though there were several worth naming: better error messages when a client-only API gets used inside a server component by mistake, more mature patterns for passing data down through the tree without prop-drilling serialized state everywhere, and tooling that makes the client-server boundary visible in the editor rather than something you have to reason about abstractly. It was the accumulation of all of it over a couple of years that eventually crossed a threshold, where the friction of getting the boundary right dropped below the friction of the old default-to-client-component habit.
The place this still occasionally trips up newer engineers is state that seems obviously client-side until it isn't, like a filter that could plausibly be encoded in the URL and handled server-side rather than in local component state. We've started catching that pattern specifically in code review, since it's the one recurring case where the old client-first instinct still shows up, and it's worth a specific callout rather than assuming everyone has fully internalized the new default just because the framework nudges them toward it. Outside of that one recurring rough edge, the transition has genuinely been as uneventful as we'd hoped it would be when we first started experimenting with the pattern.
The other change worth mentioning is how this has affected our approach to data fetching more broadly. With server components as the default, a lot of the client-side data-fetching abstractions we built up over the years, custom hooks wrapping fetch calls, loading-state management, cache invalidation logic tied to client lifecycle, have simply become unnecessary for a large share of our features. Data gets fetched directly where it's needed on the server, and a lot of what used to be meaningful architectural surface area in a typical feature's code has quietly disappeared, not because we replaced it with something better but because it stopped needing to exist at all.
That disappearance has been a genuinely pleasant side effect rather than something we planned for. Code reviews for new features are shorter now, partly because there's less code to review, and partly because the code that remains tends to be more directly readable: a server component that fetches data and renders it, with none of the loading-state juggling that used to pad out even a fairly simple feature. We didn't fully appreciate how much of our React codebase's complexity was actually client-fetching scaffolding until that scaffolding started disappearing on its own.