If you've been building for the web for a while, you probably remember the days
when making a page interactive meant sprinkling <script> tags throughout your
HTML, hoping everything loaded in the right order, and wrestling with global
variables. If you wanted a dynamic search box or a fancy dropdown, you'd either
write a bunch of jQuery or wire up some custom JavaScript, and then cross your
fingers that it played nicely with your backend-rendered HTML.
Fast forward to today, and React's new Server Components model takes a
completely different approach. Now, you can compose your UI out of both server
and client components, letting each do what it does best. And the best part? You
get to include interactive client components right in your server-rendered
UI—no script tag gymnastics required.
Let's dig into why this is such a big deal, and how it works in practice.
The Old Way: Script Tag Soup
Back in the day, if you wanted a page that showed a list of products (rendered
on the server) and let users filter them with a search box (handled on the
client), you'd end up with something like this:
<!-- Server-rendered HTML -->
<input type="search" id="search" />
<!-- Client-side search functionality -->
<script src="/static/search.js"></script>
window.initSearch({ selector: 'ul' })
You had to make sure your scripts loaded after the HTML, and that your
JavaScript could find and hook into the right DOM nodes. If you wanted to pass
data from the server to the client, you'd often have to serialize it into a
<script> tag or a data-* attribute. It worked, but it was fragile and hard
to scale.
The React Server Components Way: Seamless Composition
With React Server Components (RSCs), you can build your UI as a tree of
components, some of which run on the server (fetching data, rendering markup),
and some of which run on the client (handling interactivity, managing state).
The magic is that you can compose these together—server components can render
client components as children, and everything just works.
Here's what that looks like in practice:
import { SearchBox } from './search-box.js' // a client component
export default async function ProductList() {
const products = await fetchProductsFromDatabase()
{products.map((product) => (
<li key={product.id}>{product.name}</li>
The SearchBox is a client component—maybe it uses state, effects, or context.
But you can include it directly in your server-rendered UI. No need to wire up
script tags or global variables. React takes care of sending just the code
needed for SearchBox to the client, and hydrates it so it's fully interactive.
Why This Is Awesome
- No More Script Tag Indirection: You don't have to manually connect your
server-rendered HTML to your client-side code. Just compose components.
- Encapsulation: Each component (server or client) owns its own data
fetching, state, and logic. No more prop drilling or global variables just to
get data where it needs to go.
- Performance: Only the interactive bits get sent to the client as
JavaScript. The rest is just HTML, streamed from the server. Less JS = faster
loads.
- Streaming and Suspense: You can use React's streaming and Suspense
features to show loading states for slow data, and have parts of your UI load
in as they're ready. This used to be a nightmare with traditional server
rendering.
A Real Example: Ships, Search, and Streaming
In the
EpicReact.dev Server Components workshop,
we build a UI that lets users search for spaceships and view details. Here's how
the composition works:
- The server component fetches the list of ships and renders the overall
layout.
- The client component handles the search box, managing its own state and
updating the UI as the user types.
- When the user searches, the server streams back the updated list, and the
client component stays interactive the whole time.
No script tags. No manual hydration. Just components.
Juxtaposition: How Far We've Come
Let's compare the two approaches:
| Old Way (Backend Framework) | React Server Components |
|---|
| Server renders HTML | Server renders UI (as React elements) |
| Client JS finds DOM nodes | Client components are included directly |
| Data passed via script tags or data-* | Data fetched where it's needed |
| Manual hydration | Automatic hydration of client components |
| Hard to scale, fragile | Encapsulated, composable, robust |
Bonus: No More Prop Drilling
One of the biggest pain points in React apps has always been prop
drilling—passing data through layers of components just to get it where it's
needed. With RSCs, you can fetch data right where you need it (thanks to async
server components), and you can use platform features like Node's
AsyncLocalStorage to avoid prop drilling for global values. (Check out the
workshop exercise on this
for more.)
Conclusion
Composing server and client components together is the modern React's
superpower. You get the best of both worlds: fast, data-rich UIs from the
server, and rich interactivity from the client—without the pain of the old days.
If you haven't tried building with React Server Components yet, now's the time.
Your future self (and your users) will thank you.
Want to learn more? Check out the
EpicReact.dev Server Components workshop
for hands-on exercises and deep dives into these concepts.