Every day, JavaScript developers, teams, and companies look for ways to make building web applications easier and improve performance. This has led to the development of many JavaScript libraries and frameworks, including React.
Recently, the React team introduced a new type of component: React Server Components (RSCs). Since then, React developers have been trying to understand what they do, why they are needed, and how they work.
This guide aims to explain everything about RSCs and how they differ from Server-Side Rendering (SSR).
#What is server-side rendering, and how does it work?
In early versions, React relied heavily on client-side rendering (CSR), where JavaScript generated all the HTML content in the browser. This approach worked well for small applications, but the initial load times increased as web apps grew in complexity.
Here’s an example of what a typical HTML file might look like in a client-side rendered React application:
<!DOCTYPE html><html><head><title>My React App</title></head><body><div id="root"></div><script src="/static/js/bundle.js"></script></body></html>
In this setup:
- The browser first loads a basic HTML file with an empty
<div id="root"></div>
. - The
bundle.js
script contains all the necessary JavaScript to mount and run the React application, including React itself, third-party dependencies, and the application code.
Once the JavaScript has been downloaded and parsed, React takes over and dynamically creates all the DOM nodes for the application, injecting them into the <div id="root">
.
The problem with this approach is that it takes time to download, parse, and execute all that JavaScript. During this process, the user sees a blank screen, which was the norm in the early days of React, where everything relied on the client's browser to render the entire application.
To tackle these performance issues, the React team introduced SSR, which aimed to speed up the initial page load by generating the HTML on the server and sending it to the client. The client would receive a fully formed HTML document instead of an empty one.
This meant that users could see the content almost immediately, significantly improving the user experience. Here's how SSR works in a nutshell:
By moving the initial rendering process to the server, SSR ensures that users get a faster, more responsive experience. However, SSR alone wasn't enough to address all performance challenges. Issues like the need to fetch all data from the server before rendering and the downloading of large JavaScript bundles persisted.
To make things even faster, React introduced features like Suspense. With Suspense, the server can send parts of the HTML immediately and load other parts as needed in the React component. This means some parts of the website can be used while others are still loading, making the whole experience smoother and quicker.
For instance, you can wrap components that depend on data fetching using Suspense to show a loading indicator. This way, React allows the browser to display the rest of the page without waiting for the entire data load.
But with all these improvements, we're still trying to ensure users don't see a white screen and have some interactive display while data is being loaded. We're bouncing between the server and the client to load our app efficiently.
Imagine a situation where we could avoid the back-and-forth by fetching data or handing heavy tasks directly to the server as the React application loads. This is where RSCs come in…
#What are React server components?
React server components render entirely on the server. Unlike traditional components, which render both on the server (in SSR) and the client, server components never make it to the client. They execute on the server, generate HTML, and send this HTML to the client, but the actual component logic remains on the server.
Here's a basic example of a server component:
import db from 'prisma-client';async function Homepage() {const products = await db.product.findMany();return (<><h1>Trending Products</h1>{products.map((item) => (<article key={item.id}><h2>{item.title}</h2><p>{item.description}</p></article>))}</>);}export default Homepage;
In this example, the Homepage
component runs on the server, fetches data from a database, and renders the HTML, which is then sent to the client. This process eliminates the need for client-side data fetching and reduces the JavaScript bundle size, improving performance.
Editor's Note
async
function. This is because async
components are a new server component feature that allows you to await
in the render.Previously, with client components, you would have needed an API route:
// pages/api/products.js (API route)import db from 'prisma-client';export default async function handler(req, res) {const products = await db.product.findMany();res.status(200).json(products);}
And then manage state in the client-side code:
// components/ProductList.js (Client Component)"use client";import { useState, useEffect } from 'react';function ProductList() {const [products, setProducts] = useState([]);const [isLoading, setIsLoading] = useState(true);useEffect(() => {async function fetchProducts() {const response = await fetch('/api/products');const data = await response.json();setProducts(data);setIsLoading(false);}fetchProducts();}, []);if (isLoading) return <p>Loading...</p>;return (<><h1>Trending Products</h1>{products.map((product) => (<article key={product.id}><h2>{product.title}</h2><p>{product.description}</p></article>))}</>);}export default ProductList;
How do server components differ from client components?
While server components run exclusively on the server, client components are the traditional components we are familiar with in React.
These components run on the server (during SSR) and the client, and they can handle client-side interactions, state management, and side effects. For example, you cannot use React hooks like useState, useEffect, or event handlers like onClick()
in server components because they don't support state management, side effects, or browser-specific APIs. These components are rendered once on the server and do not re-render on the client.
It’s also important to know that with the introduction of RSCs, all components are now assumed to be server components by default. We must opt-in for client components by specifying 'use client'
at the top of the component.
#Benefits and limitations of RSCs
Although RSCs offer significant performance and architectural benefits, they also come with their own set of limitations. Let’s explore the benefits and limitations of RSCs.
Benefits of React server components
Improved performance: RSCs reduce the JavaScript bundle size by not including their logic in the client-side bundle. This results in faster load times and better performance because the client has less JavaScript to download and execute.
Simplified data fetching: RSCs can directly fetch data from databases or APIs on the server, reducing the need for multiple client-server round trips. This results in more efficient data fetching and faster response times.
Better user experience: Since the server handles heavy computations and data fetching, the client can focus on rendering and interactivity, resulting in a quicker Time to Interactive (TTI) and a smoother user experience. Users see the content almost immediately as the server sends fully rendered HTML, reducing the perceived load time and improving user engagement.
SEO benefits: Since RSCs generate HTML on the server, search engines can easily crawl and index the content, improving SEO performance and visibility in search results. Consequently, websites can achieve higher search engine rankings and attract more organic traffic.
Limitations of React server components
Limited interactivity: RSCs cannot use React hooks like
useState
oruseEffect
, limiting their ability to manage client-side state and user interface interactivity. They are unsuitable for components requiring user interaction, as they cannot handle client-side events directly. Developers must carefully design their applications to separate interactive elements from static content.Fragmented ecosystem: Currently, RSCs are primarily supported by frameworks like Next.js 13.4+, and other frameworks may not yet fully support RSCs. This can lead to compatibility issues and limited adoption.
Learning curve: RSCs introduce a new way of thinking about component rendering and data fetching. Developers familiar with traditional client-side rendering may face a learning curve when adopting RSCs. Coordinating between server and client components, especially in complex applications, can be challenging and require careful planning and design.
Editor's Note
'use client'
directive.#A guide to using React server components
To use React server components, you need a compatible framework. Currently, only Next.js 13.4+ supports RSCs using the new App router.
To help you understand, let's make an API request to Hygraph, a graphql-based headless CMS, in a Next.js 14 application.
You can spin up a Hygraph project easily by logging into your dashboard and cloning the Hygraph “Basic Blog" starter project. You can also choose any project from the marketplace that suits your needs.
Next, go to the Project settings page, navigate to Endpoints, and copy the High-Performance Content API endpoint. We'll use this endpoint to make API requests in our React project.
If you haven't already, create a new Next.js project:
npx create-next-app@latest my-appcd my-app
Next, to interact with Hygraph's GraphQL API, install the graphql-request
library:
npm i graphql-request
Create a .env
file in the root of your project and add your Hygraph API URL as an environment variable.
NEXT_PUBLIC_HYGRAPH_API_URL=your-hygraph-api-url
In your Next.js project, you can now make this API request without needing any hook:
import { request } from 'graphql-request';const query = `{posts {idslugtitleexcerptcoverImage {url}publishedAtauthor {namepicture {url}}}}`;const fetchBlogPosts = request(`${process.env.NEXT_PUBLIC_HYGRAPH_API_URL}`,query);
You can then receive the data within the component:
const page = async () => {const { posts }: any = await fetchBlogPosts;return (// ...);};
This is the complete code:
import { request } from 'graphql-request';const query = `{posts {idslugtitleexcerptcoverImage {url}publishedAtauthor {namepicture {url}}}}`;const fetchBlogPosts = request(`${process.env.NEXT_PUBLIC_HYGRAPH_API_URL}`,query);const page = async () => {const { posts }: any = await fetchBlogPosts;return (<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5">{posts.map((currentPost: any) => (<div className="relative h-[440px] mb-5" key={currentPost.id}><imgclassName="w-full h-52 object-cover rounded-lg"src={currentPost.coverImage.url}alt=""/><h2 className="text-xl font-semibold my-4">{currentPost.title}</h2><p className="text-gray-600 mb-2">{currentPost.excerpt}</p><div className="flex justify-between items-center absolute bottom-0 w-full"><div className="flex items-center"><imgclassName="w-10 h-10 rounded-full object-cover mr-2"src={currentPost.author?.picture.url}alt=""/><p className="text-sm text-gray-600">{currentPost.author?.name}</p></div><p className="text-sm text-gray-600">{new Date(currentPost.publishedAt).toLocaleDateString('en-us', {year: 'numeric',month: 'short',day: 'numeric',})}</p></div></div>))}</div>);};export default page;
This way, the server component fetches data (blog posts) from the Hygraph API and renders it on the server. When you load your page, you will notice the data is loaded immediately as the page loads. This is because the server generates the HTML with the data and sends it to the client, so there's no need to wait for additional client-side JavaScript to fetch and render the data.
This approach significantly improves performance and ensures your content is quickly visible to users.
#Integrating React server components with other libraries
With the growing adoption of RSCs, many third-party data-fetching packages now work well with RSCs, as seen with the graphql-request
package. While some integrations can still be tricky, the ecosystem is rapidly evolving, and developers are working hard to make all packages compatible with RSCs.
Libraries like TanStack Query and SWR are widely used in React applications for efficient data fetching and caching. There have been discussions on how these would work with RSCs, but it’s still challenging and not fully supported, particularly since SWR is a hook that runs on the client side.
However, if you consider data fetching from Hygraph, you can always use the graphql-request
package. Additionally, the enhanced fetch API introduced in Next.js 13 works harmoniously with server components, making it easier to handle data fetching and caching without relying heavily on external tools.
#Wrapping up
Developers must stay informed about updates and best practices as RSCs continue to evolve. RSCs are a game changer, and incorporating them into your web development toolkit can substantially improve performance, SEO, and data handling, ultimately enhancing the user experience across various applications.
The ability to offload heavy computations and data fetching to the server reduces client-side complexity, making your applications faster and more efficient.
For a powerful way to create, store, and manage content, consider using Hygraph. You can start by signing up for a free-forever developer account.
Blog Author