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.
Before getting into RSCs, it's helpful to understand earlier rendering strategies, such as React Suspense and SSR.
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:
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:
Suspense
and ErrorBoundary
, which adds repetitive code and makes it harder to keep everything consistent and maintainable as your app grows.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:
SSR also has limitations to be considered:
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:
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.
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.
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"],
},
},
];
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;
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!
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.
Although RSCs come with benefits, it's also important to understand the obstacles and boundaries that come with incorporating them into your project.
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.
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.
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.
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.
There are practical solutions you can implement to maintain consistency and address common RSC issues.
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.
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.
Addressing hydration mismatches requires proactive testing and debugging techniques:
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.
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.