React Server Components — Part I of III
React Had a Data Problem. We All Just Lived With It.
I remember the exact moment I realized React had a problem.
It was 2023. I was building a dashboard — nothing exotic, just a page with a user profile at the top, their recent orders in the middle, and some activity stats at the bottom. Standard enterprise stuff. I opened the network tab to check performance.
The requests went out like a staircase.
User profile first. Then, once that came back and the component re-rendered, the orders fetch kicked off. Then stats. Each one waiting for the previous one to finish before it even started. I had just built a textbook waterfall. And the embarrassing part? I'd been building them for years without fully realizing it.
The pattern we all used
If you've been writing React for any amount of time, you've written this:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`) // or whatever fancy hook you use
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}This pattern was so common it became muscle memory. And it worked — in the sense that the app ran and users could use it. But there was a cost hiding in there that we mostly ignored.
The cost was that fetching happened after rendering. The component had to mount, React had to run its reconciliation, the browser had to paint something, and only then — after all of that — did the actual data request go out.
And if that component had children that also needed their own data? Each child would go through the same process: render first, fetch later. The tree had to walk all the way down before it even knew what data it needed.
What a waterfall actually looks like
Here's the thing about waterfalls — they're not obvious while you're writing the code. The individual component looks fine. The useEffect makes sense in isolation. The problem only becomes visible when you zoom out and look at what happens across the entire component tree at runtime.
This is what that looks like in practice. Hit replay if you want to see it again.
fetches happen one after another
all fetches run in parallel on the server
In the "without RSC" column, each fetch can only start after the component above it has received its data and re-rendered. The total time is the sum of every individual fetch. On the right side, all three server fetches happen in parallel — and only one payload comes back to the browser.
That's a real difference. Not a micro-optimization, not a benchmark edge case. A structural difference in how data moves through your app.
Why didn't we fix this earlier?
We tried. Sort of.
React Query and SWR came along and they genuinely helped. Caching, deduplication, background refetching — all of it made the day-to-day experience better. But they were solving for developer ergonomics, not the waterfall itself. The waterfall was still there. You just had nicer tools for dealing with it.
Relay went further — it's a full data-fetching framework built around GraphQL fragments that can be colocated with components and batched into a single query. It actually solves the waterfall problem. But it also requires you to buy into a significant amount of complexity, and it only works with GraphQL. Most teams weren't willing to pay that price.
So the majority of React apps kept doing fetch-on-render, kept building waterfalls, and kept wondering why their loading states looked like dominoes falling.
The real problem was deeper than data fetching
Here's what I think was actually going on.
React's component model was always designed around the browser. Components are JavaScript functions, they run in JavaScript engines, and for the first several years of React's existence, the assumption was: you're on the client. Always.
SSR (server-side rendering) existed, yes. Next.js made it accessible. But traditional SSR still shipped all your component code to the browser — it just also did an initial render on the server to get the HTML faster. Your UserProfile component, your OrderList component, your StatsCard component — all of that JavaScript still ended up in the browser bundle, whether the user was ever going to interact with it or not.
Think about how many components in your app are purely presentational. They receive props, they return JSX, they have no onClick handlers, no local state, no browser-specific behavior. There's no reason those components need to run in a browser. But they always did — because the model didn't give you a choice.
Data fetching was tangled up in this too. If your component runs in the browser, it can't talk directly to your database. So it has to call an API. And that API call can only happen after the component mounts. Which means you're fetching after rendering. Which means waterfall.
The whole system was self-reinforcing. Client-first rendering led to API-only data access, which led to fetch-on-render, which led to waterfalls.
What would actually fix it?
The React team's answer, which took a few years to fully materialize, was to flip the model. Instead of assuming all components run on the client, what if some components ran only on the server? What if you could write a React component that talked directly to your database, sent only the rendered output to the browser, and shipped zero JavaScript?
That's React Server Components.
Not a new framework. Not a replacement for everything you know about React. Just a way to say: this component lives on the server. It can do server things. And the browser never needs to know it existed.
Before you jump to Part II — one important warning. A lot of people hear "runs on the server" and immediately think "oh, like SSR." It's not. The distinction matters more than you'd expect, and confusing them leads to a lot of the frustration I see people have with RSC. Part II starts with exactly that.
What's next
In Part II, I'm going to break down exactly how RSC works under the hood — not the marketing version, but what actually happens when React renders a server component, what it sends to the browser, and why it's fundamentally different from anything that came before it.
The short version: React doesn't send HTML. It doesn't send JavaScript. It sends something in between, and understanding that format is what makes everything else click.