Skip to main content
Elevating user experiences with React Server Components
19 Oct 2023

Elevating user experiences with React Server Components

Imagine interacting with a website that feels sluggish, unresponsive, ore confusing. Now, picture an experience that's intuitive, responsive, and anticipates your needs. That's the power of user experience (UX) in action. In today's digital landscape, a positive UX is no longer nice to have; it's a necessity. React Server Components (RSC) emerge as a powerful tool to elevate UX by enabling faster loading times, improved interactivity, and a more seamless user journey.

What are React Server Components?

React Server Components flawlessly mix the power of server-side rendering with the interactive charm of client-side JavaScript. They take care of complicated tasks in the background, allowing developers to focus on creating exceptional user experiences. What's truly remarkable is that RSCs, despite their advanced functionality, are essentially the same React Components you've been using all along. Imagine them as dependable helpers in the world of web development. They handle the tough work, so developers can focus on creating amazing digital projects.

Let’s delve deeper into the world of React Server Components, exploring the specific issues they address, the reasons for their emergence, and the exciting features they offer.

Why React Server Components ?

To understand why RSCs are used, let’s understand at the challenges that arise in a typical React code and the reasons driving the necessity for React Server Components.

Let’s look at this example:

const App = () => {
    return (
        <ProductWrapper>
            <ProductList />
            <Testimonials />
        </ProductWrapper>
    )
}

The above example shows two components - <ProductList/> and <Testimonials/> that are passed to the <ProductWrapper/> component as child props.
Now, let's look at the body of each component.

<ProductWrapper/>

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

  useEffect(() => {
   // API call to get data for Wrapper component to function and
   set it to local state
   //For testing purpose
    setTimeout(() => {
      setWrapperData("ProductWrapper");
    }, 1000);
  }, []);

  // Only after API response is received, we start rendering
  // ProductList and Testimonials (children props)
  return (
    <>
      <h1>{wrapperData}</h1>
      <>{wrapperData && children}</>
    </>
  );
};

<ProductList/>

const ProductList = () => {
  const [listData, setListData] = useState();

  useEffect(() => {
    //API call to get data for ProductList component to function 
    and set it to local state
    //For testing purpose
    setTimeout(() => {
      setListData("ProductList");
    }, 3000);
  }, []);

  return (
    <>
      <h1>{listData.name}</h1>
    </>
  )
}

<Testimonials/>

const Testimonials = () => {
  const [testimonialsData, setTestimonialsData] = useState();

  useEffect(() => {
   //API call to get data for ProductList component to function
    and set it to local state
   //For testing purpose
    setTimeout(() => {
      setTestimonialsData("Testimonials");
    }, 2000);
  }, []);

  return (
    <>
         <h1>{testimonialsData.name}</h1>
       </>
  )
}

As evident, every component is tasked with fetching its individual data. Let's consider that each component requires a specific duration to initiate API calls and retrieve data from the server.

  • <ProductWrapper/> takes 1 sec to respond
  • <ProductList/> takes 2 sec to respond
  • <Testimonials/> takes 3 sec to respond

React Server Component (RSC)

The problems that occurred: 

1. User experience problem - 

<ProductWrapper/> is rendered after one second. <ProductList/> is rendered after two seconds. And after three seconds, <Testimonials/> appears. But <Testimonials/> enters the view by pushing <ProductList/> down. This is not a great user experience.

<ProductList/> and <Testimonials/> both components must wait until the <ProductWrapper/> fetches the data and gets rendered on screen, which results in a waterfall. This means, until and unless a parent component doesn't get its response, the child component must wait to get rendered on the screen.

2. Maintanability -

const App = () => {
    const data = fetchAllData();
    return (
        <ProductWrapper data={data.ProductWrapperData}>
            <ProductList data={data.ProductListData} />
            <Testimonials data={data.testimonialsData} />
        </ProductWrapper>
    )
}


As of now, the App component has undergone modifications. It now initiates a request that fetches all the necessary data in a single operation. Subsequently, this fetched data is then distributed to the respective components by passing it along as props.

Imagine a scenario where, in future, the developer decides to eliminate the <Testimonials/> component from the application. If, however, they unintentionally overlook removing a portion of the <Testimonials/> API implementation from the backend, an interesting situation arises. Even though the <Testimonials/> component no longer exists within the application, the backend can generate responses related to Testimonials. This circumstance gives rise to a significant issue regarding "maintainability". Essentially, devs end up receiving redundant data, which can complicate the overall management and upkeep of the application.

3. Cost

Using libraries is like a treat for developers, but it can make the app heavier and slower. Many parts of an app are like calm waters, and there’s no need for constant splashes from user actions. Imagine a "details" page where you just soak in info about a product or a user – it doesn't need to dance to every user tap. In Server Components, developers can go wild with third-party packages without worrying about their bundle size puffing up.

Basic architecture diagram of Client Components and Server Components

Client Component:

React Server Component

Server Component:

React Server Components

The solution is React Server Components

Server components are designed to tackle the problems mentioned above. To quickly sum it up, components on the user's end ask the server for information, and one must wait for the server's response before showing more parts of the app. This can cause a cascade effect like waterfalls when there's a sequence of these requests happening.

To simplify it, move the data gathering to the server for speed. Data fetching from the server is way faster than fetching from the user’s device. Sounds good, right? But there's a catch. Developers cannot use familiar tools like hooks (useState, useEffect), Web APIs (localstorage), or actions like clicking a button (onClick) as usually done in React. This is because Server Components are built on the server side. They work great when real-time updates or user actions aren't a must-have.

Take a quick look at what a Server Component appears like:

import ProductEditor from 'ProductEditor';

async function Product(props) {
  const {id, isEditing} = props;
  //can directly access server data sources during render, e.g. 
  database
  const Product = await db.posts.get(id);

  return (
    <div>
      <h1>{Product.title}</h1>
      <section>{Product.body}</section>
      {isEditing 
        ? <ProductEditor Product={Product} />
        : null
      }
    </div>
}

Did you notice a React Component making a call to a database? This is possible because of a Server Component called <Product/>. It's like any other React component, but with some unique powers, as shown in the example above (direct access to a database). But remember, one cannot call the database directly inside the Client Components. (Regular React Code).

Now, let's focus on the <Product/> component. But, what about the <ProductEditor/> component inside the <Product/> component? Here's what the body of the <ProductEditor/> component looks like.

'use client';

import { useState } from 'react';

export default function ProductEditor(props) {
  const Product = props.Product;
  const [title, setTitle] = useState(Product.title);
  const [body, setBody] = useState(Product.body);
  const submit = () => {
    // ...save note...
  };
  return (
    <form action="..." method="..." onSubmit={submit}>
     //something
    </form>
  );
}

In the above code, 'use client' is used to declare this component as a client component.

Whenever 'use client' is used at the beginning of a file, that component is known as a Client Component. If 'use client' isn't used, then the component in that file is treated as a Server Component.

To summarize -

const ServerComponentA = () => {
    return (
        <ClientComponent>
            <ServerComponentB />
        </ClientComponent>
    )
}
  • Client Components can be imported inside Server Components.
  • Server Components cannot be imported inside Client Components.
  • A Server Component can be passed as a child prop to a Client Component inside the Server Component as shown in above code snippet.
  • It is recommended to user Server Components as the root component where you fetch all the data from server and keep your Client Components as leaves of the tree where it will rendered on the browser.

Wrapping up

If you're itching to dive into Server Components, hitch a ride with Next.js 13 – it's your ticket to the world of these magical components. Making server components the go-to choice is like giving Next.js 13 a cool makeover. So, get your creative cap on and enjoy some playful experimentation with Server Components in the fantastic Next.js 13 playground.

Subscribe to our feed

select webform