JSX Evolved: The React Server Components

JSX Evolved: The React Server Components
Photo by Ross Sneddon / Unsplash

React's journey over the last ten years has been nothing short of revolutionary.

This popular library has been in a constant state of growth, with each new version bringing fresh ideas and performance boosts. Some updates have even changed how we think about building web applications.

The latest big shake-up in the React world is React Server Components (RSC). It's arguably the most important update we've seen since React introduced hooks, which was a game-changer itself.

Many influential React developers are excited about RSCs, saying they're crucial for React's future. But as with any major change, the React community is split – some developers are thrilled, while others are more cautious about this new direction.

React's new design supports React Server Components, which run only on the server and make the most of server-side rendering.

But let's be honest - we're all used to how React works now, so this big change naturally makes some of us nervous or doubtful.

While server and client components bring great improvements and more options to React development, they also come with new ideas that can initially be tricky to understand.

In this blog post, I will explain how React's rendering has changed over time.

I will show you why React Server Components aren't just a new change - they're the future of building React apps that are cost-effective, super fast, and give users a great experience. By the end, you'll see why this new approach is so important for React developers.

Client-side rendering (CSR)

Remember when React was all about Single Page Applications (SPAs)?

Let's take a trip down memory lane and see how things have changed.

In the classic SPA setup, when you visit a website, the server sends a bare-bones HTML page to your browser. This page is pretty empty, as it only contains a single div tag with a "root" id and a link to a JavaScript file. This JavaScript file is the real star of the show - it contains everything your app needs, including React itself and all your app's code.

Once your browser downloads this JavaScript, it gets to work. It generates all the HTML for your app right there in your browser and plugs it into that empty div. That's why you can see the full HTML in your browser's inspector tool, but not when you view the page source - the source only shows what the server initially sent.

This approach is called client-side rendering (CSR), and it quickly became the go-to method for SPAs. But as with anything in tech, people started noticing some downsides:

  1. SEO struggles: Search engines prefer content-rich HTML. A nearly empty initial HTML file doesn't give them much to work with. By the time the JavaScript creates all the content, search engine crawlers might have already moved on.
  2. Performance issues: Putting all the work on the user's device (the client) can slow things down. Users might stare at a blank screen or a loading spinner while their browser crunches through all that JavaScript. This gets worse as apps grow and that JavaScript file gets bigger.
  3. Slow connections suffer: If you're on a crowded WiFi network or a slow mobile connection, you will wait for some time before seeing anything useful.

CSR was a game-changer and paved the way for the interactive web apps we love today.

But to fix these SEO and speed bumps, developers started hunting for better solutions. And that's where our story of React's evolution gets interesting...

Server-side rendering (SSR)

React frameworks like Next.js found a clever way to fix the problems with client-side rendering (CSR).

They flipped the script and moved much of the work back to the server.

In this approach, instead of sending a nearly empty HTML file and making the user's browser do all the heavy lifting, the server does the initial work. It creates a full HTML document with all your content and sends that to the browser. This means the browser can show something meaningful right away, without waiting for a bunch of JavaScript to run first.

This server-side approach solves 2 big CSR problems:

  1. SEO boost: Search engines love this because they can easily read and index all server-generated content.
  2. Faster initial load: Users see actual content quickly, not just a blank screen or spinner.

But here's where it gets interesting. While the page looks ready, it's not fully interactive yet.

To make buttons clickable, forms fillable, and everything else work, we still need to download and run some JavaScript. This process is called hydration.

Think of hydration like this: the server sends a detailed painting of your app, and then the browser brings that painting to life.

During hydration, React takes over in the browser. It looks at the HTML the server sent and builds a matching structure in memory. Then it adds all the interactivity - setting up click handlers, initializing the app's state, and making everything respond to user actions.

This approach gives us the best of both worlds: fast initial loads for users and search engines, plus all the interactivity we love about modern web apps.

There are 2 main flavors of this approach: Static Site Generation (SSG) and Server-side Rendering (SSR). Both aim to solve the problems of client-side rendering, but they work in slightly different ways.

Think of SSG as pre-cooking your meals for the week. When you deploy your app, SSG creates all the pages in advance. These pre-made pages are ready to serve instantly when a user requests them. It's perfect for content that doesn't change often, like blog posts or product pages.

On the other hand, SSR is more like ordering a meal. When a user requests a page, the server whips it up on the spot. This is great for personalized content, like social media feeds, where what you see depends on who you are.

Often, you'll hear that people group these two approaches under the SSR umbrella. Both are huge improvements over client-side rendering, offering faster initial loads and better SEO.

But here's the catch: while SSR solved some problems, it brought its own set of challenges. It's like fixing a leak in your roof only to find that your new solution is causing dampness in the walls.

While Server-Side Rendering (SSR) solved some problems, it also introduced new challenges. Let's break down three main issues:

  1. The "All Data or Nothing" Problem: Imagine you're baking a cake. With SSR, you can't start mixing ingredients until you have every single one ready. If you're missing even one ingredient, you have to wait. Similarly, SSR can't start building a page until all the data is ready. This can slow down how quickly the server responds to the browser.
  2. The "Mirror Image" Requirement: Think of this like a game of "Spot the Difference". For SSR to work properly, the page structure in the browser must exactly match what the server created. This means your browser needs to download all the JavaScript for every component on the page before it can start making anything interactive.
  3. The "No Stopping" Hydration: Hydration is like bringing a frozen pond to life. Once it starts defrosting, it doesn't stop until the whole pond is liquid. Similarly, React's hydration process goes through the entire page in one go. You can't interact with any part of the page until everything is hydrated.

These three issues create what we call a "waterfall" problem. It's like a series of dominoes - each step has to finish before the next one can start.

This can be inefficient, especially if some parts of your app are slower than others (which is often the case in real-world applications).

Recognizing these limitations, the React team went back to the drawing board. They've come up with a new and improved SSR architecture to address these challenges.

React Suspense

React 18 brought a game-changer: Suspense for Server-Side Rendering (SSR). This new feature aims to fix the performance issues we saw with traditional SSR.

The star of the show is the <Suspense> component. It unlocks 2 powerful features:

  1. HTML streaming on the server
  2. Selective hydration on the client

I said above that traditional SSR was like baking a cake where you needed all the ingredients before you could start. Well, HTML streaming flips this on its head.

In the old world of SSR, here's what happened:

  1. The server renders the entire HTML
  2. The server sends the complete HTML to the client
  3. The client shows this HTML
  4. The client waits for all JavaScript to load
  5. React hydrates the whole app to make it interactive

It was like waiting for a full-course meal to be prepared before you could take a single bite.

HTML streaming changes this. It's more like a buffet where dishes come out as they're ready.

The server can now send parts of the HTML as soon as they're rendered, without waiting for the whole page to be ready.

This means users can start seeing content faster, even if some parts of the page are still loading. It's a huge step forward in making React apps feel snappier and more responsive.

Here's the process:

  1. We wrap the main content area of our page in a Suspense component.
  2. This tells React, "Hey, don't wait for this part to be ready. Start sending the rest of the page right away!"
  3. Instead of the main content, React initially sends a placeholder (like a loading spinner).
  4. The server keeps the connection open, like a live feed.
  5. As soon as the main content is ready, React sends it through this live feed.
  6. Along with the content, React sends a tiny bit of JavaScript that knows exactly where to put this new content.

The cool part? Users start seeing most of the page super fast. Then, the main content pops in as soon as it's ready.

It's like getting the outline of your newspaper immediately, then watching the articles fill in in real-time.

This approach makes websites feel much faster and more responsive, especially on slower connections.

Interactivity and Code Splitting

Even with faster HTML delivery, we hit a wall.

The app can't become interactive (or "hydrate" in React-speak) until all the JavaScript loads. If your main section has a ton of JavaScript, it could slow everything down.

That's where code splitting comes in handy.

Think of code splitting like packing for a trip. Instead of stuffing everything into one big suitcase, you can use several smaller bags. In code terms, we're breaking our JavaScript into smaller chunks.

React gives us a cool tool called React.lazy.

It's like telling React, "Hey, this part isn't urgent. Load it later."

This way, the main chunk of your app's code can load without waiting for every single feature.

Here's where it gets really interesting. By wrapping sections in <Suspense>, you're permitting React to be smart about hydration. It can make parts of your page interactive as soon as their code is ready, without waiting for everything.

What this means for users:

  1. They see content quickly (thanks to HTML streaming).
  2. Parts of the page become interactive as soon as possible.
  3. Heavy features don't hold up the whole show.

Imagine a webpage that loads like a progressive image.

First, you see the structure, then it fills in with content, and finally, interactivity spreads across the page like a wave. That's what we're achieving here.

This approach is a huge leap forward in making React apps feel lightning-fast, even with complex features and slow connections.

This selective hydration also solves the "all or nothing" problem. Instead of waiting for the whole page to become interactive, React starts making parts of it usable right away.

For example, you can use the header or side menu while the main content is still loading. React handles this process on its own, so developers don't need to worry about it.

However, even with Suspense with SSR, there are some issues with how web apps in React currently work:

  1. Download bloat - as apps get fancier, users have to download more and more code. Do they really need all that data?
  2. Over-hydration - right now, React makes every part of a page interactive, even if it doesn't need to be. This wastes resources and slows things down. Should we really make everything interactive?
  3. Client-side heavy lifting: Most hard work happens on the user's device, not the powerful servers. This can make things sluggish, especially on older devices. Shouldn't we use our servers more?

These problems are too big for small fixes. We need a new approach to solve them effectively - react server components.

React Server Components (RSC)

React Server Components (RSC) are a new way of building React apps. They combine the best parts of server-side and client-side rendering.

RSC introduces two types of components:

  1. Client components - these run in the browser.
  2. Server components - these run on the server.

The main difference isn't what these components do, but where they run and what resources they can use. This setup helps make React apps faster to load and more interactive.

Client Components

Client components are just the React components we've been using all along.

They usually run in the browser, but can also be pre-rendered on the server for faster initial page loads.

You can think of client components as browser-focused, even if they sometimes run on the server. Their main job is still to handle things in the user's browser.

What can client components do?

  • Use React features like state and useEffects
  • Handle user interactions
  • Access browser-only tools like geolocation or localStorage

The term "Client Component" isn't new tech - it's just a way to distinguish these familiar components from the new server components in React's latest architecture.

Here is an example of a client Power component:

"use client"

export default function Power() {
  const [power, setPower] = useState(0);

  return (
    <div>
      <p>{power}</p>
      <button onClick={() => setPower(power + 1)}>Add +1 power</button>
    </div>
  );
}

Server Components

Server Components are a new type of React component that run exclusively on the server.

Their code never reaches the client, which brings several advantages:

  1. Smaller downloads - no JavaScript is sent to the browser for these components which is great for users with slow internet or less powerful devices
  2. Direct backend access - it can talk directly to databases or file systems
  3. Better Security - keeps sensitive stuff (like API keys) off the client
  4. Smarter data fetching - this mechanism avoids the "waterfall" problem of nested data fetching. It moves sequential data fetching to the server for better performance
  5. Caching boost - server-side rendering results can be cached and reused, which is great for static or slowly changing data
  6. Faster initial load - generates HTML on the server, pages appear quicker, especially good for content-heavy sites
  7. SEO Friendly - search engines can easily read the server-rendered HTML
  8. Efficient Streaming - sends page chunks as they're ready and users see content sooner, no waiting for full page render

In essence, Server Components bring the server power to React, making apps faster, more secure, and more efficient.

Let's look at a practical example of a Server Component in action.

We'll create the PokemonList page that shows how these components work:

export default async function PokemonList() {
  const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=10&offset=0");
  const pokemons = res.json();

  if(pokemons.results.length === 0){
    return <p>No pokemons found!</p>;
  }

  return (
    <main>
        <ul>
          {pokemons.results.map((pokemon) => (
            <li key={pokemon.id}>
              {pokemon.name}
            </li>
          ))}
        </ul>
    </main>
  );
}

React server components (RSC) are shaking things up in React.

They're bringing together the best of server-side and client-side React, helping developers build faster, more efficient apps using familiar React tools.

The big switch is that everything is a server component now.

In the new RSC world, especially in Next.js apps, every component starts as a server component by default.

This is a big change from how we used to do things.

The "use client" directive

So, how do we create client components in this new setup? We use a special instruction called "use client".

Here's what you need to know:

  1. "use client" tells React, "Hey, this component (and any components it imports) should run in the browser."
  2. we add "use client" at the top of the component file.
  3. it gives your component access to browser features and lets it handle user interactions.

Think of "use client" as a passport that lets your component cross from the server world to the client world.

Rendering lifecycle

Let's break down how React Server Components (RSC) work their magic, using Next.js as our example.

We'll look at 3 key elements:

  • your browser,
  • Next.js (on the server),
  • React (also on the server).

Here is how the initial loading works:

  1. Browser asks for a page - your browser requests a URL and Next.js finds the right server component for that URL.
  2. React gets to work (on the server) - React renders the server component and its server component children. It creates a special JSON package (called the RSC payload). If a server component isn't ready, React sends a placeholder.
  3. Client components get ready - instructions for client components are prepared for later use.
  4. Next.js builds the page - using the RSC payload and client component instructions, Next.js creates HTML. This HTML is sent to your browser right away for a quick preview.
  5. Streaming in action - as React finishes each piece of the UI, Next.js streams it to the browser.
  6. Browser Starts Assembly - your browser processes the incoming React data. It uses the RSC payload and client component instructions to build the UI piece by piece.
  7. Final Touches - Once everything loads, users can see the complete UI. The client components "wake up" (hydrate), making the page interactive

This process turns a server-side render into a fully interactive web page. It combines the speed of server rendering with the interactivity of client-side React.

Refreshing the page sequence

When part of your app needs to update, here's what happens:

  1. The browser asks for a refresh - your browser requests an update for a specific part of the UI (like a whole route).
  2. Server springs into action - Next.js finds the right server component and React renders the component tree, similar to the initial load.
  3. Streamlined response - unlike the first load, no HTML is generated this time. Next.js streams the new data directly to the browser.
  4. Browser updates the view - Next.js triggers a re-render of the route with the new data.
  5. React's smart merge - React blends the new output with what's already on the screen. This preserves important UI states (like where you're focused or what you've typed).

Conclusion

React server components are changing the game in a way that they handle data fetching and static content, while client components take care of interactive elements.

The beauty of this system is that it combines the best of server-side and client-side rendering. You get to use:

  • One language (JavaScript/TypeScript)
  • One framework (React)
  • One set of tools and APIs

This approach improves on traditional rendering methods while solving many of their problems.