Exploring the Power of React Server Components and React Suspense

Exploring the Power of React Server Components and React Suspense

React and Next.js have launched many updates this year, and among them, React Server Components and React Suspense have sparked significant discussion in the developer community. If you're not yet familiar, React Server Components allow for more efficient rendering by offloading certain tasks to the server, while React Suspense offers a way to handle asynchronous operations more smoothly in your React applications. Intrigued? Let's dive deeper into these concepts

React Server Components

The essence of React lies in its ability to provide composability and reusable custom building blocks. These components can be used across different parts of an application with minimal need for reconfiguration.

Consider a scenario where you have a Post component. This component takes postId as a prop and fetches and displays post content from an API. Traditionally, this process might look something like this:

const Page = () => {
  return (
    <div>
      <Post postId={"post-1"} />
      <Post postId={"post-2"} />
      <Post postId={"post-3"} />
    </div>
  );
};

const Post = ({ postId }) => {
    useEffect(() => {
         // data fetching from an API using fetch or axios   
     }, [])
  return <>
  // displaying result
  </>;
};

Before Server Components, data fetching was often client-side, leading to visible loading states (like spinners) and reliance on the client's network speed and we used to get data using fetch like below.

const Post = ({ postId }) => {
    const [loading, setLoading] = useState(false)
    const [data, setData] = useState()

    useEffect(() => {
    setLoading(true)
    fetch(`/api/post/${postId}`)
        .then(res => {
            setData(res.data)
        })
    setLoading(false)
    }, [])
  return <>{loading ? <Spinner /> : <div>{data.post}</div>}</>;
};

Using this way of data fetching, when we load our application on the client side, it will be rendered quickly. But once the page is rendered we will see Spinner when the data is being fetched on the client side. The data fetching on the client side may take longer than that depending on the network speed as well.

Now, let's say we want to move data fetching to the server side instead of the client side to avoid having Spinner and dependency on the client's network to fetch the data. In that case, we'll use getServerSideProps and it will look like the below.

export const getServerSideProps = async ({ params }) => {
  const { data } = await sql`SELECT * FROM POSTS WHERE id=${params.id}`;

  return {
    props: {
      data: data.post,
    },
  };
};

const Page = ({ data }) => {
  return (
    <div>
      <Post postData={data} />
    </div>
  );
};

const Post = ({ postData }) => {
  return <div>{postData}</div>;
};

Using this approach, as data is being fetched on the server inside getServerSideProps, when we load the application in the browser, we will see the browser loading state longer than the previous one. The reason is that it is fetching all the data on the server and due to that, we will not see the Spinner on the client side as well after the page has been rendered.

This approach is good and efficient in many ways but it makes our Post component dependent on the getServerSideProps. So, if we want to move our Post component somewhere else or maybe just reuse it elsewhere in a relatively lower child component then it will be dependent on the parent component to fetch and pass data to our Post component. Thus, it loses its composability which was the main feature of React in the first place. You will get more clarity by looking at the diagram below.

As shown in the above diagram, our Post component is not completely composable. Whenever we're trying to reuse it, it is expecting data from the parent component and thus it's dependent on it.

So, how do we make our component composable without being dependent on the server-side API and render it on the server side? Here is where React Server Components come into play. So, let's consider the above code block and make it a server component and also make sure that it is composable.

const ServerCompPost = async ({ postId }) => {
  const { data } = await sql`SELECT * FROM POSTS WHERE id=${postId}`;
  return <div>{data.post}</div>;
};

ServerCompPost is a server-side component, self-sufficient in fetching its data. By default, every component in Nextjs is a server component, thus we don't have to do anything additional here to make it a server component. You can refer to their docs here. Thus, we don't need to copy-paste getServerSideProps at all the places we will be using this component. It is totally composable and self-dependent for its data and doesn't require data to be passed as a prop from the parent component

React Suspense

Now, let's explore how React Suspense can further enhance user experience. Imagine a page rendering numerous <Post /> components and a <SimilarPosts /> component. <SimilarPosts /> is another server component and is fully composable as shown below.

const Page = () => {
  return (
    <div>
      <Post postId={"post-1"} />
      <Post postId={"post-2"} />
      <Post postId={"post-3"} />
      <SimilarPosts />
    </div>
  );
};

Thus, all this data fetching for all the <Post /> components and <SimilarPosts /> components are happening on the server side. Due to that, when we load our page in the browser it's taking a longer time as it is fetching data for all the components. Now, the user is gonna see Similar Posts only when he is going to scroll to the end after all the <Post />. Thus, he may or may not see Similar Posts. So, we can optimize that by server rendering that component only when the user scrolls to the bottom and that component is in the viewport.

We can achieve that using Suspense. We need to wrap our component with the Suspense wrapper and show <Spinner /> as a fallback. Below is the updated code.

const Page = () => {
  return (
    <div>
      <Post postId={"post-1"} />
      <Post postId={"post-2"} />
      <Post postId={"post-3"} />
      <Suspense fallback={<Spinner />}>
        <SimilarPosts />
      </Suspense>
    </div>
  );
};

With Suspense, we defer rendering SimilarPosts until it's actually in view. This not only improves initial load times but also optimizes resources.

Wrapping Up and Looking Ahead

In summary, React Server Components and React Suspense offer powerful ways to optimize both the performance and the user experience of web applications. By leveraging these features, developers can build more efficient, responsive, and user-friendly applications.

As we continue to see advancements in these technologies, it's exciting to imagine how they will shape the future of web development. Have you experimented with these features in your projects? Share your experiences and insights in the comments below!