Blurring the Boundary Between Server and Client
React Server Components (RSC), introduced in Next.js, change how client-server interactions work. RSC blurs the traditional boundary between the two, making it possible for server-side components to handle data fetching and rendering, while reducing the amount of work done in the client. This article will break down how RSC functions, the advantages it offers, and how it impacts the way you develop React apps.
Traditional Server-Client Architecture
In traditional web applications, the server and client operate separately:
- Backend (Server):
- Runs on a server (e.g., Node.js).
- Handles logic, interacts with databases, and serves data via APIs .
- Frontend (Client):
- Built with library like react.
- Makes API calls to fetch data and renders the UI in the browser.
- Manages state, user interactions, and handles rendering after receiving data from the server.
Example of Traditional Client-Server API Call:
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data') // Fetch data from backend API
.then(response => response.json())
.then(result => setData(result)); // Handle and render data on the client
}, []);
return (
<div>
{data.map(item => (
<p key={item.id}>{item.name}</p>
))}
</div>
);
This model separates the frontend and backend completely. The frontend fetches data from APIs and then renders the UI in the browser.
What Are React Server Components?
React Server Components (RSC) change this setup by running some components on the server. With RSC, you no longer need to fetch data with API requests from the client in many cases. Instead, server-side components can fetch data and send rendered HTML directly to the client, reducing client-side overhead and JavaScript bundle size.
Server Components vs Client Components
In RSC, components are divided into two types:
Server Components:
- These are rendered on the server.
- They handle data fetching and return HTML directly to the client.
- Server components do not ship to the browser, so they reduce client-side JavaScript.
Client Components:
- These run in the browser.
- Client components manage state and handle user interactions, like button clicks or form submissions.
By rendering parts of the UI on the server, React Server Components reduce the amount of JavaScript the client needs to execute, leading to better performance.
Example of Server and Client Components in RSC:
// ServerComponent.js (Server Component)
export default async function ServerComponent() {
const data = await fetchDataFromDatabase(); // Server-side fetching
return <div>{data.name}</div>; // Rendered on the server
}
// ClientComponent.js (Client Component)
'use client'; // Explicitly marked as a client component
import { useState } from 'react';
export default function ClientComponent() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
- ServerComponent fetches data on the server and sends rendered HTML to the client.
- ClientComponent handles state changes and user interactions in the browser.
Benefits of React Server Components
React Server Components introduce several advantages to your app:
Improved Performance:
- Since server components do not run in the browser, you send less JavaScript to the client, improving page load times.
Reduced API Overhead:
- Traditional applications rely on APIs to fetch data. With RSC, data fetching can be handled on the server side, eliminating the need for separate API calls from the client.
Smaller JavaScript Bundles:
- Only the client components responsible for user interactions are sent to the browser, reducing the overall bundle size.
How Component Instances Work
In RSC, how components behave depends on where they are used. If a server component imports another component, it will run on the server. However, if a client component imports a server component, the server component will now run in the client, unless explicitly marked otherwise.
Example of Component Behavior:
// App.js (Top-level server component)
import ServerComponent from './ServerComponent';
import ClientComponent from './ClientComponent';
export default function App() {
return (
<>
<ServerComponent /> // Runs on the server
<ClientComponent /> // Runs on the client
</>
);
}
If ServerComponent imports another component, it will remain a server component and run on the server. If ClientComponent imports the same component, that component will now run on the client. Here's how that works:
// ServerComponent.js
export default function ServerComponent() {
return <NestedComponent />; // NestedComponent runs on the server
}
// ClientComponent.js
'use client';
import NestedComponent from './NestedComponent';
export default function ClientComponent() {
return <NestedComponent />; // NestedComponent runs on the client
}
This setup provides flexibility. You can reuse components in different environments depending on where they are rendered.
Data Fetching Without APIs
One of the biggest advantages of RSC is that you no longer need to rely on traditional API calls from the client for data fetching. Server components can fetch data directly and handle rendering in one step.
Example: Server-Side Data Fetching in RSC:
// ServerComponent.js
import { getDatabaseData } from './database'; // Fetch data directly from database
export default async function ServerComponent() {
const data = await getDatabaseData(); // Server-side fetching
return <div>{data.name}</div>; // Render UI with fetched data
}
This approach removes the need for a client to make separate API requests, reducing complexity and improving efficiency.
Conclusion
React Server Components fundamentally change the relationship between server and client-side rendering. By allowing components to run on the server, RSC simplifies data fetching and reduces the amount of JavaScript sent to the client, leading to faster, more efficient applications. With less reliance on traditional APIs and a smaller client-side footprint, RSC offers a streamlined development approach that improves both performance and maintainability.