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:
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.