Master React server components with our comprehensive guide
React has completely transformed how we create user interfaces by introducing a component-based design that encourages efficiency and reusability. Its declarative method and component-based structure have gained popularity among developers. But as your projects grow in complexity, you might encounter performance challenges because of rendering and data-retrieval methods.
In the traditional methods of rendering on the client side, you depend on the browser to run JavaScript for showing the initial user interface. This may cause delays in loading on devices with limited resources. Fetching data from an application programming interface (API) introduces additional load on these devices, which can cause a lag in displaying content and providing less-than-ideal user interactions.
Server components offer a solution to these obstacles by enabling the rendering of components on the server. This allows the server to stream the components to the client side, which enhances the performance and the data-retrieval processes since all the processing happens on the server and not on the users' devices.
In this article, you will learn how React's rendering methods have evolved over time, as well as the drawbacks of using React Suspense and server-side rendering (SSR). Additionally, you'll learn about React Server Components (RSCs) and how they tackle these issues.
Understanding traditional React rendering strategies
Before getting into RSCs, it's helpful to understand earlier rendering strategies, such as React Suspense and SSR.
React Suspense
React Suspense was introduced to handle asynchronous rendering in React applications. It allows components to "suspend" rendering until certain conditions are met, such as data fetching or code splitting. Here are some of the advantages of Suspense:
- Improved user experience. Suspense allows you to have placeholders for your components, which makes it easy to show loading indicators when data is being fetched. This helps in avoiding blank content showing on the user's screen.
- Component rendering. When you have components fetch data from an API, Suspense pauses the rendering of the component until the data is available. This prevents partially loaded pages and helps users have a better experience.
- Code splitting. Suspense works with features like
React.lazy
to enable code splitting and improve performance by loading components only when they're needed.
But React Suspense has some limitations to be aware of as you scale your application:
- Incomplete support for server-side data fetching. Suspense makes client-side data loading easier, but it doesn't support data fetching on the server side. To manage data retrieval from the server side, you need to use tools such as React Query. For instance, with Suspense, you still have to handle tasks like server-side caching and make client-side API requests to fetch data, which can add complexity to SSR.
- Increased complexity with nested components. Working with Suspense in nested components can get quite complex at times. For example, when you're developing a product page that displays reviews, recommendations, and stock information, the coordination between loading the data for these nested components with Suspense boundaries can lead to slower loading times. This can complicate the code as each component's data dependencies and loading states must be managed.
- Boilerplate code overhead. Suspense often requires extra code to handle loading states, errors, and fallbacks. For example, in a large app, you might end up wrapping many components in
Suspense
andErrorBoundary
, which adds repetitive code and makes it harder to keep everything consistent and maintainable as your app grows.
Server-side rendering
When it comes to SSR, React components are rendered on the server, and then the fully generated HTML is sent to the client's side for display. In contrast, with RSCs, individual components are rendered when they are ready instead of waiting for the whole application to load, as is the case of SSR. This approach has multiple advantages:
- Improved initial load times. By pre-rendering the HTML on the server, you allow the browser to show content much faster. For example, if you're building an e-commerce site, users can see product details right away while the rest of your JavaScript loads in the background. This makes your app faster, especially if the users have a slow connection or device.
- SEO advantages. Search engines can easily categorize the rendered HTML code of your website, which enhances its visibility in search results. For instance, if you have a blog or a content-rich website, using SSR assists in guaranteeing that search engines promptly access and evaluate your articles without delay for JavaScript loading. In this case, SSR enables you to have a better SEO ranking.
SSR also has limitations to be considered:
- Hydration overhead. After the server sends the initial HTML, the browser has to "hydrate" it by attaching event listeners and making the page interactive. This can cause performance issues because the browser has to render the page twice—once for the static content and again for the interactive parts. For example, if applications have features like analytics dashboards with graphs and input fields, this multiple rendering of different components can cause delays for users.
- Larger payloads. With SSR, you have to send the entire JavaScript bundle to the client, which can increase the payload size. For example, when you're building a large app with multiple features, the amount of JavaScript sent to the browser can increase significantly, which may slow down page load times, especially on slower connections.
- Resource-intensive. SSR has to generate HTML for each request made to it, leading to increased processing requirements and elevated server expenses. For example, if your application faces a traffic surge, you may need to boost server resources, adding complexity and cost to infrastructure management.
React Server Components
RSCs are a new type of component in React that operates on the server side, unlike traditional components, which operate on the client side. They help address the issues associated with Suspense and conventional SSR by enabling components to be processed and streamed as HTML from the server to the client. RSCs also handle data retrieval on the server, ensuring that confidential information, such as API keys, is not revealed to the client.
RSCs provide some significant advantages:
- Optimized performance. With RSCs, you move the rendering work to the server, reducing the amount of JavaScript your users' browsers need to run. For example, if you're building a blog, HTML content can be streamed directly, allowing posts to appear almost immediately without waiting for JavaScript processing.
- Secure data fetching. RSCs fetch data server-side, enhancing both speed and security. For example, in an e-commerce app, the server retrieves product data, keeping API keys hidden from the client.
- Reduced client-side JavaScript execution. Offloading rendering to the server reduces JavaScript execution on user devices, enhancing performance, especially on slower devices. In a dashboard, for instance, the server processes data-heavy components, so minimal JavaScript is needed on the client side.
- Faster initial page loads. Since RSCs stream pre-rendered HTML, content loads quickly for users. On a news site, articles and images display instantly, avoiding blank screens while JavaScript loads.
RSCs vs. traditional React components
Compared to client-side components, RSCs handle execution, data management, and performance in different ways.
Traditional components run in the browser (client-side), where JavaScript is needed to display the user interface (UI). RSCs run on the server by sending pre-rendered HTML to the client device instead of processing everything locally. This approach lessens the workload on the user's device and improves loading speed. Handling layouts and static content server-side is also ideal for noninteractive or data-driven components. Client components can be nested within RSCs where interactivity is needed, such as for buttons or forms, while keeping sensitive data secure on the server.
Traditional components can increase the size of the client-side JavaScript bundle, which could lead to slower app performance. RSCs do not increase the client's package size as they operate on the server side, leading to reduced downloads and quicker page loading times. You can also use RSCs to improve caching. Server-rendered content is easier to cache, which speeds up responses for frequently accessed data, like popular products in an online store, without repeatedly querying the database.
How to implement RSCs
To implement RSCs in your project, follow these steps to set up your environment and start using both server-side and client-side components effectively.
Project setup
First, ensure you have the latest Node.js installed on your machine. The first step is to create a new React project from your shell or terminal:
npx create-react-app your-app --template cra-template-pwa
cd your-app
Once your project is initialized, install the dependencies required for RSCs:
npm install react@18 react-dom@18 babel-loader@8 @babel/preset-react webpack@5 webpack-cli
For more information, you can check the official documentation related to each one of these libraries: React, react-dom, webpack, Babel, and webpack-cli.
Run the following command to install the loaders for CSS and asset files like SVGs, which will allow your application to process and include stylesheets and media files:
npm install css-loader style-loader file-loader --save-dev
Set up Babel by creating a .babelrc
file in the root of your project with the following content:
{
"presets": ["@babel/preset-react"]
}
Create a new file named webpack.config.js
in your project's root directory and add the following configuration to set up webpack:
const path = require("path");
module.exports = [
// Server-side configuration
{
entry: "./src/server.js", // Server entry point
target: "node",
output: {
path: path.resolve(__dirname, "dist"),
filename: "server.js",
},
module: {
rules: [
{
test: /\.jsx?$/,
use: "babel-loader",
exclude: /node_modules/,
},
{
test: /\.css$/, // Rule for CSS files
use: ["style-loader", "css-loader"],
},
{
test: /\.(png|jpe?g|gif|svg)$/, // Rule for image files
use: ["file-loader"],
},
],
},
resolve: {
extensions: [".js", ".jsx"],
},
},
// Client-side configuration
{
entry: "./src/index.js", // Client entry point
target: "web", // This tells Webpack to bundle for the browser
output: {
path: path.resolve(__dirname, "dist"),
filename: "client.js", // Client-side bundle
},
module: {
rules: [
{
test: /\.jsx?$/,
use: "babel-loader",
exclude: /node_modules/,
},
{
test: /\.css$/, // Rule for CSS files
use: ["style-loader", "css-loader"],
},
{
test: /\.(png|jpe?g|gif|svg)$/, // Rule for image files
use: ["file-loader"],
},
],
},
resolve: {
extensions: [".js", ".jsx"],
},
},
];
Creating your components
With webpack configured, you can now create a file named server.js
inside the src
folder with the following code to set up your server entry point:
import React from "react";
const express = require("express");
const ReactDOMServer = require("react-dom/server");
const App = require("./App").default;
const app = express();
// Serve the static files from the 'dist' folder (where Webpack will put client.js)
app.use(express.static("dist"));
app.get("*", (req, res) => {
const appHtml = ReactDOMServer.renderToString(<App />);
res.send(`
<!DOCTYPE html>
<html lang="en">
<head><title>React Server Components</title></head>
<body>
<div id="root">${appHtml}</div>
<!-- Include the client-side bundle -->
<script src="/client.js"></script>
</body>
</html>
`);
});
app.listen(3000, () => console.log("Server running on port 3000"));
In the src
folder, create a new file called UserList.server.js
to set up a basic server component that fetches data:
import React from "react";
function UserList() {
// Dummy array of users
const users = [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Smith" },
{ id: 3, name: "Alice Johnson" },
];
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default UserList;
This component displays user data and returns an HTML list. To add client-side interactivity, create a file called Counter.client.js
in the src
directory for your client component:
import React, { useState } from "react";
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<button
onClick={() => {
setCount((prevCount) => {
const newCount = prevCount + 1;
return newCount;
});
}}
>
Increment 1
</button>
<p>Count: {count}</p>
</div>
);
}
export default Counter;
This way, the server initializes the state, and the client handles the interactivity. You've now set up a complete workflow for RSCs, created a server component, passed data to a client component, and enabled client-side interactivity.
Next, update src/App.js
to include the following code, which integrates both server and client components:
import React from "react";
import UserList from "./UserList.server";
import Counter from "./Counter.client";
function App() {
return (
<div>
<h1>Users</h1>
<UserList />
<Counter initialCount={50} />
</div>
);
}
export default App;
Run and test the code
Now, when the server renders the App component, it will also render the UserList
Dashboard
as a server component and send the HTML to the client.
To run your server, bundle the code and start it:
npx webpack
node dist/server.js
Running these commands will return Server running on port 3000
. At this point, your app is rendering React components on the client and the server!
Best practices for using RSCs
Since RSCs handle most of the rendering on the server, shifting as much of your app's rendering as possible to the server can significantly improve performance while keeping interactive features intact where needed. The following are some best practices that can help you achieve this and enhance your application's speed, flexibility, and manageability with RSCs.
First, use RSCs to fetch data from APIs or databases. If you've already fetched data on the server, don't fetch it again on the client. For example, if you load user data on the server and pass it to the client, there's no need for a second API call on the client side unless the data changes.
Breaking code down into small chunks and selectively loading the needed scripts using tools such as Vite or webpack can help improve load times. Create components that can be reused across your app. For example, if multiple pages need to display a list of users, create a UserList
component that works everywhere.
Finally, keep server components lightweight by focusing them purely on rendering data as they don't need to manage state like client components do. Focus on making them lightweight and purely responsible for rendering data. Only hydrate the parts of a page that need to be interactive. For example, if the page is mostly static but has a form or interactive button, hydrate only those components.
Challenges and limitations of RSCs
Although RSCs come with benefits, it's also important to understand the obstacles and boundaries that come with incorporating them into your project.
State management
Server components cannot interact with lifecycle methods such as useEffect
or useState
. This means you can't manage client-side interactivity within server components. For example, if a form is rendered server-side, you'll need a client component to handle real-time input changes.
When you're dealing with both client- and server-side components in your projects, it's important to handle the interaction between these two types of components across your application to avoid any issues. For example, if your application fetches data from the server and lets users edit this data locally on their devices, you might face issues with keeping the data consistent between the server and the client-side components.
Non-serializable code
Server components can only use serializable props, meaning you can't pass functions, symbols, or any complex objects that can't be serialized. For instance, sending a callback function from a server component to a client component will lead to an error message. Server-side components don't have access to browser APIs such as window
, document
, or localStorage
. If you need to interact with the DOM, that must happen in a client component.
Hydration mismatches
If the HTML displayed by the server doesn't align with what the client-side JavaScript expects, it may lead to problems during the hydration process. For example, differences in how dates are formatted on the server vs. the client can cause subtle mismatches. Hydration mismatches can be hard to debug because the root cause isn't always obvious.
Third-party library compatibility
Some third-party libraries may not work well with RSCs, particularly those that heavily depend on client-side rendering techniques or browser specific APIs. For example, libraries such as react-router-dom
require client side rendering. You may need to replace or adapt some libraries to make them work with RSCs.
Solutions for common RSC issues
There are practical solutions you can implement to maintain consistency and address common RSC issues.
Use consistent state management
Use state management tools that work across both server and client, like Redux or Zustand. These libraries allow you to centralize your state so that server-rendered components and client-side components can access the same data. For example, you could use Redux to store a global state that's available to both sides of the app.
Ensure that you maintain a flow of data between the server and client by sharing context. For instance, when moving user data between server and client components, make sure the information is managed centrally to avoid any discrepancies.
Use non-serializable code and refactor components
Consider using development tools or code linters to identify non-serializable properties that might cause issues with RSCs. For instance, tools such as ESLint can be set up to detect function props that are transmitted from server-based components.
Make sure to refactor components to ensure that props can be serialized and avoid any issues with SSR. One way to do this is by moving certain operations like event handlers to client components. For example, when passing a callback function from a server component, make sure the code is adjusted so that the operation happens within the client component where it's being used.
Handle hydration mismatches
Addressing hydration mismatches requires proactive testing and debugging techniques:
- Strict mode. Turn on React's strict mode to catch problems in the development process. Strict mode will point out errors as you work so you can fix them before they become issues in the final product.
- Thorough testing. Ensure thorough testing plans are in place to detect any problems related to hydration. Test automation can mimic server-side and client-side rendering processes to maintain uniformity. Consider using tools such as Cypress or Jest to confirm that the output displayed is consistent across both server and client environments.
Ensure third-party library compatibility
If third-party software does not work with server components as needed for your project's requirements, explore alternative options that are server compatible. If you use an open source library for your project, you might want to think about giving back by contributing to its development to include RSC support as well. By contributing, you can enhance your own application and also have a positive impact on the broader community that relies on the same library for their work.
By tackling these challenges with proper strategies in place, you can efficiently navigate the limitations of RSCs and build robust, performant applications.
Conclusion
In this article, we explained React's rendering techniques. We introduced React Suspense and SSR and provided a more in-depth guide to RSCs. You can use RSCs to help enhance the performance of your application by decreasing the need for client-side JavaScript processing and accelerating the initial page loads.
If you want to try out RSCs in your projects, the first step is to pinpoint the components in your app that would gain advantages from SSR. You can then slowly incorporate RSC into your codebase.
Running the infrastructure for SSR and server components can be complex. Using Upsun, a platform as a service (PaaS), simplifies deploying and scaling your React applications. With its built-in Node.js support and automatic scalability, Upsun can help you handle traffic surges smoothly. It also provides monitoring and security tools and allows you to manage multiple applications or microservices in different languages within a single project. This way, you can focus on building your React application while Upsun handles the core infrastructure.