A Comprehensive Guide for React Server Components

Relia Software

Relia Software

Huy Nguyen

Relia Software

featured

React Server Components (RSCs) revolutionize front-end development by enabling server-side rendering of UI components. How to incorporate RSCs into React projects?

A Comprehensive Guide for React Server Components

Table of Contents

Have you ever experienced a frustratingly slow loading time while using a React SPA? This is a common challenge with Single Page Applications (SPAs), where the browser handles a significant workload. This can lead to a less than ideal user experience, with users waiting for pages to load or encountering sluggish interactions.

Thankfully, React has evolved to address these issues. Server-side rendering strategies like Static Site Generation (SSG) and Server-Side Rendering (SSR) have been developed to lighten the client's load. But the latest innovation, React Server Components (RSCs), takes it a step further. By enabling server rendering at the component level, RSCs offer a powerful mix of server-side efficiency and client-side interactivity. This article aims to guide developers on how to incorporate RSCs into their React projects.

>> Read more about React coding:

React Server Components Explained

React Server Components (RSCs) revolutionize front-end development by enabling server-side rendering of UI components.

By utilizing Server Components, tasks such as data retrieval and database updates occur directly on the server. This direct link to the data source eliminates the requirement for repetitive back-and-forth communication between the client and server. As a result, your application can efficiently fetch data and pre-render components on the server simultaneously.

Consider this React Server Component example:

import getPosts from '@/lib/getVideoCourse'
import CreatePostButton from '@components/create-post' // client component

export async function ServerComponent(){
  const posts = await getPosts()

  return(
    <>
      {posts.map((post, index) => {
        <Post key={index} post={post} />
      })}
      <CreatePostButton />
    </>
  )
}

In this setup, we can fetch data from an external source asynchronously and pre-render the entire content on the server. The created HTML template is then smoothly integrated into the client-side React tree. Server Components have the ability to import Client Components, as demonstrated with the CreatePostButton import.

The use of Server Components brings numerous performance advantages. They never undergo re-rendering, which leads to quicker page load times. Unlike rendering methods such as SSR and SSG, the HTML produced by RSCs is not hydrated on the server, and no JavaScript is sent to the client. This greatly enhances page load speed and minimizes the total JavaScript bundle size.

Client Components VS Server Components

In React, components are categorized based on their rendering location and capabilities. Here's a breakdown of Client Components and Server Components:

  • Client Components: These components are rendered on the client-side (user's browser). They have full access to the browser environment and can manage state (using hooks like useState) and respond to user interactions (event listeners). This allows for dynamic and interactive UIs.
  • Server Components: These components are rendered on the server. They primarily focus on data display and lack access to the browser environment. Server Components cannot directly utilize React Hooks (like useState or useEffect) or interact with standard web APIs.

Examples below will illustrate the difference between Client Components and Server Components in React.

Client Component

import React, { useState } from 'react';

function ClientComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default ClientComponent;

In this ClientComponent, we’re using the useState hook to manage state. When the button is clicked, the state is updated, and the component re-renders to reflect the new state. This is an example of a Client Component because it involves user interaction and state management on the client side.

Server Component

import React from 'react';

function ServerComponent({ data }) {
  return (
    <div>
      <h1>{data.title}</h1>
      <p>{data.description}</p>
    </div>
  );
}

export default ServerComponent;

In this ServerComponent, we’re simply rendering data passed in as props. There’s no state management or user interaction involved. This component could be rendered on the server and sent to the client as HTML. This is an example of a Server Component because it doesn’t involve any client-side interactivity or state management. It simply displays data.

Remember, React Server Components are still experimental and not yet available in a stable release. The actual usage and syntax might change in the future. Please refer to the official React documentation for the most up-to-date information.

Understanding Client Components in Next.js

>> Read more: Next.js: Empowering Web Development with React

Client Components in Next.js are essentially the familiar React components we’ve been using. However, with Next.js 13, all components default to Server Components. To specify a component as a Client Component, we need to add the “use client” directive at the start of the file.

Once this is done, the component can then use event handlers and client-side Hooks like useStateuseContextuseEffect, and others. The code example below demonstrates a Client Component using useState to manage loading state for a button:

// CreateButton.tsx
"use client"
import { useState } from 'react'
import { newPost } from '@lib/addNewPost'

const [isLoading, setIsLoading] = useState(false)

export default function CreateButton(){
  return (
    <button onClick={newPost({ /* new post */ })}>
      {isLoading ? 'Loading...' : 'New Post'}
    </button>
  )
}

It’s crucial to understand the interaction between client and server components. In the initial Server Component we discussed, you might have noticed the CreatePostButton Client Component being incorporated.

There’s a misconception that Client Components are unable to import Server Components. This isn’t entirely true. However, if a Server Component is nested within a Client Component that employs the "use client" directive, the Server Component is effectively transformed into a Client Component.

To correctly incorporate a Server Component into a Client Component, adopt the following method, utilizing the children prop:

"use client"
export default function ClientComponent({ children }){
  return (
    <div>
      {children}
    </div>
  )
}

Next, you can go ahead and embed a Server Component within the Client Component:

// page.tsx (server component)
import ServerComponent from './ServerComponent'
import ClientComponent from './ClientComponent'

export default function Home(){
  return(
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

For a more detailed understanding of how to intertwine server and client components, you can refer to this video, which provides a clear explanation.

The secret to employing the appropriate component type is to understand the structure and needs of each component in your application. The Next.js documentation includes a table that illustrates the correct scenarios for using server and client components.

Usage of React Server Components in Next.js 13

Next.js 13 offers a powerful combination of React Server Components (RSCs) and the new App Router. This enables you to build performant and interactive applications with the latest React features. Additionally, Next.js 13 includes an improved fetch API for efficient data caching and memoization of repeated requests.

To kick off a new Next.js project, simply execute the command npx install create-next-app@latest and select the App Router during setup.

As a standard setting, all components within the Next.js App Router are Server Components.

// app/page.tsx
export default function Home() {
  console.log('Server Component')
  return (
    <main>
      <h2>server component</h2>
    </main>
  )
}

Here's a basic example of a Server Component that fetches and displays a list of JavaScript video tutorials from a JSON server as a local database operating on localhost:4000.

// app/page.tsx
interface Video {
  id: int
  image: string
  title: string
  views: string
  published: string
}

async function videos() {
  const videos = await fetch('http://localhost:4000/videos').then((res) =>
    res.json()
  )
  return videos
}

export default async function Home() {
  const vids: Video[] = await videos()

  return (
    <>
      {vids.map((video, index) => (
        <li className='mb-6' key={index}>
          <a
            href={`https://www.youtube.com/watch?v=${video.id}`}
            target='_blank'
            rel='noopener noreferrer'
            className='hover:opacity-40'
          >
            <Image
              src={video.image}
              alt={video.title}
              width={400}
              height={200}
              className='mb-4 rounded-md'
            />
            <h4>{video.title}</h4>
            <div>
              {video.views} &bull; {video.published}
            </div>
          </a>
        </li>
      ))}
    </>
  )
}

This example demonstrates how an RSC can asynchronously fetch data on the server and render the content efficiently. You can explore the provided repository for the complete implementation with video details and styling.

Remember, RSCs offer numerous advantages for building modern web applications. Explore the resources available online to delve deeper into their capabilities and best practices.

Enhancing SSR Performance with React Suspense and Streaming

Traditional server-side rendering (SSR) can suffer from waterfall-like content delivery, where the entire page needs to be rendered before the user sees anything. In other words, all asynchronous requests must be completed, and the user interface must be constructed before client-side hydration can occur. This can lead to long load times for complex applications.

Thankfully, React Suspense and React Streaming Components (RSCs) come to the rescue, improving SSR performance.

React Suspense allows us to pause the rendering of a component within the React tree. Instead of showing a blank screen, it displays a loading component while fetching content in the background. As soon as the content becomes available, it seamlessly replaces the loading UI within the component wrapped by Suspense.

Here's an example using Suspense with a custom loading component:

import { Suspense } from 'react'
import SkeletonScreen from './loading'

export const async function Home(){
  const posts = await fetchPosts()

  return (
    <Suspense fallback={SkeletonScreen}>
      {videos.map(post => (
        // UI...
      ))}
    </Suspense>
  )
}

Next.js incorporates Suspense into the App Router using a unique loading.js file. This file is designed to automatically encapsulate the page.js file with Suspense and display the custom UI in loading.js. Below is a simulated depiction of the React tree:

<Layout>
  <Suspense fallback={Loading.js}>
    <Page />
  </Suspense>
</Layout>

Instead, we have the option to establish this file and inscribe the Loading component here:

// app/loading.tsx
export default function SkeletonScreen() {
  return (
    <>
      {Array(5)
        .fill(5)
        .map((item, index) => (
          <li className='my-5' key={index}>
            <div className='bg-[#DDDDDD] rounded-md w-[400px] mb-4 h-[190px] '></div>
            <div className='bg-[#DDDDDD] rounded-md h-[15px] w-2/3 mb-2'></div>
            <div className='bg-[#DDDDDD] rounded-md h-[15px] w-11/12 mb-2'></div>
            <div className='bg-[#DDDDDD] rounded-md h-[15px] w-1/2'></div>
          </li>
        ))}
    </>
  )
}

Now, the fallback will be displayed without the necessity of directly utilizing the Suspense component.

RSCs optimize performance cost in some case. Let’s consider a simple example where we have a large list of items that we want to display on a webpage. In traditional client-side JavaScript, we might do something like this:

let data = []; // Assume this is a large array of data
let html = '';
for(let i = 0; i < data.length; i++) {
    html += `<div>${data[i]}</div>`;
}
document.getElementById('app').innerHTML = html;

In this case, the entire list is rendered in the browser. If the list is very large, this can cause performance issues as the browser struggles to render all the items at once.

RSCs tackle this challenge by utilizing streaming under the hood. This means the server can send the HTML to the browser in chunks, rather than waiting for the entire list to be rendered. Now, let’s see how we can solve this with React Server Components:

import {pipeToNodeWritable} from 'react-server-dom-webpack/writer';
import fs from 'fs';
import path from 'path';
import React from 'react';
import {renderToPipeableStream} from 'react-dom/server';

function App() {
    let data = []; // Assume this is a large array of data
    return (
        <div>
            {data.map(item => <div key={item.id}>{item}</div>)}
        </div>
    );
}

const outStream = fs.createWriteStream(path.resolve(__dirname, './output.html'));
pipeToNodeWritable(<App />, outStream);

In this case, the server does the heavy lifting of rendering the list, and the browser simply displays the already-rendered HTML. This can significantly improve performance for large lists.

Pros and Cons of Using React Server Components

Pros

Server Components also offer other benefits in web development:

  • Enhanced Performance: By transferring heavy tasks to the server, RSCs lessen the client’s workload. This results in more stable webpages and improved Core Web Vitals like Largest Contentful Paint (LCP) and First Input Delay (FID).
  • Efficient SEO: As RSCs generate HTML on the server side, search engines can easily index the content and rank the pages accurately.
  • Increased Security: Sensitive data such as auth tokens or API keys used in RSCs are processed on the server and never revealed to the browser, avoiding accidental leaks.
  • Data Fetching: Having your data source located with Server Components accelerates data fetching, leading to more responsive web experiences.
  • Reduced Client-Side Bundle Size: RSCs execute on the server, so their code doesn't contribute to the size of the JavaScript bundle downloaded by the browser. This can lead to faster initial page loads.
  • Server-Side Logic: RSCs allow you to leverage server-side logic within your components, which can be helpful for tasks like data validation or authorization.

Cons

  • Early Stage: RSCs are a relatively new feature and are still under development. The syntax and API might change in the future.
  • Limited Interactivity: Currently, RSCs don't support all React features like state management hooks (useState, useEffect) or event handlers. This can make them less suitable for highly interactive components.
  • Increased Server Load: Since RSCs handle more rendering on the server, they can potentially increase server load compared to traditional client-side rendering. This needs to be considered for high-traffic applications.
  • Debugging Complexity: Debugging RSCs can be more challenging compared to client-side components, as issues might not be readily apparent in the browser developer tools.

Code Example for Using React Server Components 

Traditional server-side rendering (SSR) can lead to a waterfall effect, where the entire page needs to be rendered on the server before the user sees anything. This can result in a slow initial load time, especially for applications that fetch data from multiple sources.

Let’s consider a scenario where we have three components: WrapperComponentA, and ComponentB. Each component fetches its own data. However, we want to avoid waterfall loading and improve user experience.

// Wrapper.server.js
import React, { useState, useEffect } from 'react';

const Wrapper = ({ children }) => {
  const [wrapperData, setWrapperData] = useState({});

  useEffect(() => {
    // Simulate API call to get data for Wrapper component
    getWrapperData().then((res) => {
      setWrapperData(res.data);
    });
  }, []);

  // Render children only after API response
  return (
    <>
      <h1>{wrapperData.name}</h1>
      {wrapperData.name && children}
    </>
  );
};

// ComponentA.server.js and ComponentB.server.js are similar
// They fetch their own data similarly

// Client components (ComponentA and ComponentB) remain unchanged
const ComponentA = () => {
  // Fetch ComponentA data
  return <h1>{componentAData.name}</h1>;
};

const ComponentB = () => {
  // Fetch ComponentB data
  return <h1>{componentBData.name}</h1>;
};

export default Wrapper;

In this example, we ensure that child components (ComponentA and ComponentB) are not rendered until the Wrapper component receives the API response. This avoids waterfalls and provides a smoother user experience.

Remember that React Server Components are still an evolving concept, and their usage may evolve over time. However, they offer exciting possibilities for improving performance and maintainability in React applications.

Integrating React Server Components (RSCs) with Other Libraries

The concept of React Server Components (RSCs) is still in its early stages, and developers have encountered challenges when integrating third-party packages with Server Components to achieve their intended functionality.

Currently, third-party components work as expected within Client Components, thanks to the “use client” directive. However, the same compatibility assurance is not readily available for Server Components.

Debates have arisen regarding the integration of well-known data-fetching libraries like React Query and SWR with Server Components. While some developers propose eliminating the need for React Query, this radical perspective may not be entirely justified. These libraries remain valuable in the ecosystem, particularly for single-page applications (SPAs).

Nevertheless, the enhanced fetch API introduced in Next 13, which is compatible with Server Components, could potentially reduce reliance on these external tools for data fetching and caching.

Additionally, React developers can explore alternative approaches to integrate libraries with RSCs:

  • Library Updates: Many popular libraries are actively being updated for better RSC compatibility. It's recommended to check for RSC-compatible versions of your preferred libraries.
  • Server-Side Rendering (SSR) Hooks: Some libraries might provide server-side rendering (SSR) hooks that allow data fetching within RSCs. Explore the documentation of your specific library to see if such hooks are available.

By considering these approaches, developers can leverage the benefits of RSCs while still utilizing their preferred data-fetching libraries when necessary.

>> You may be interested in:

Conclusion

Incorporating React Server Components (RSCs) into your web development toolkit can significantly enhance performance, SEO, and data management, ultimately improving the user experience across various applications. As this concept evolves, staying informed about the latest updates and best practices is essential to fully leverage the advantages of RSCs in your work.

>>> Follow and Contact Relia Software for more information!

  • coding
  • development