React - Server Components - Introduction and Initial Thoughts
Tags: webdev, javascript, react, frontend
Published: 2020-12-27 14:06:39

---
description: Just before Christmas, the React team gave an early Christmas present, Server Components a.k.a the **zero bundle size** components. Let's have a look at what they are, what they bring to the table and my thoughts.
---


Just before Christmas, the React team gave an early Christmas present, Server Components a.k.a the **zero bundle size** components. Let's have a look at what they are, what they bring to the table and my thoughts.

Before we start, just want to let you know that the best resource for a deeper understanding would obviously be the [RFC](https://github.com/josephsavona/rfcs/blob/server-components/text/0000-server-components.md) and the [introduction video](https://www.youtube.com/watch?v=TQQPAU21ZUw&t=2196s) from the React team. I put this together for people who are light on time and to share my thoughts and understanding.

You can find the entire source for this post [here](https://github.com/sidthesloth92/server-components-demo). It's a fork of the actual demo repo from the React team. I just simplified the components for easier understanding. All kudos go to the React team.

With the introduction of the Server Components, the existing components have been renamed as Client components. In fact, we have three types now:
* Server Components
* Client Components
* Shared Components

# Server Components
Let's look at some of the important features of the Server components.

## Zero Bundle Size
They are zero bundle size because they are rendered on the server and only the rendered content is sent to the client. This means they do not add to your client JS bundle size. Let's look at an example,

```javascript
// BlogPost.server.js - A Server component.

import { renderMarkDown } from '...'; // Server only dependency.
import {getBlogPost} from './blog/blog-api';

export default function BlogPost({blog}) {
  const blog = getBlogPost(blog.id); // Get blog post from database directly.

  return (
    <>
      <h1>{blog.title}</h1>
      <p>{renderMarkdown(blog.markdown)}</p>
    </>
  );
}
```

Things to note here,
* All server components are suffixed with `server.{js,jsx,ts,tsx)` (At least for now). 
* Since they are not sent to the client, we can have code that accesses server resources like database, internal API's etc.
* Since all this happens in the server, the package you imported for rendering the markdown is not sent to the client, only the rendered content is sent. This is a significant reduction in the Client JS bundle size.

The component itself is straight forward, it fetches the data from the database and renders the content.

## Render Format

If you have noticed, I have said that the *content* is rendered and not `HTML`. This is because Server components are not rendered to HTML but rather to an intermediate format. 

If the above component was the only component in your app, this is what would be returned from the server.

```javascript
J0: [
	["$", "h1", null, {
		"children": "Blog 1"
	}],
	["$", "p", null, {
		"children": "unt aut..."
	}]
]
```
As you can see, only the rendered markdown is sent to the client and not the library itself.

Now you might be wondering why not HTML and this format? (I don't know the format's name.. 🙃). Let's see why in the next section.

## State and difference from SSR

Let's look at a primary difference between Server components and SSR. SSR generates the HTML on the server which is then sent to the client for rendering by the browser. This means the content itself is static and you cannot have interactive markup.

However, since Server components use this intermediate format instead of HTML it allows them to have Client components that have interactive behaviour. Make no mistake, Server components themselves, **cannot have state or event handlers**, in other words, they cannot make use of `useState`, `useEffect` etc. However, they can have Client Components which in turn can have state. 

Let's add a like button to the `BlogPost` component that when clicked increments the number of likes for the blog post.

```javascript
// BlogPost.server.js - A Server component.

import {getBlogPost} from './blog/blog-api';
import LikeButton from './LikeButton.client';

export default function BlogPost({blog}) {
  const blog = getBlogPost(blog.id);
  return (
    <>
      <h1>{blog.title}</h1>
      <p>{blog.markdown}</p>
      <LikeButton blog={blog} /> // A Client component.
    </>
  );
}
```

```javascript
// LikeButton.client.js - A Client component.

import {likeBlogPost} from './blog/blog-api';
import React from 'react';

export default function LikeButton({blog}) {
  const [likesCount, setLikesCount] = React.useState(blog.likes);
  
  const handleClick = () => {
    setLikesCount(prev => prev + 1);
  };

  return <span onClick={handleClick}>Likes: {blog.likes}</span>;
}
```

The `BlogPost` Server component has a child component `LikeButton`, which is a Client component that handles user interaction. The `LikeButton` component is free to make use of `useState` as it is a Client component and it also updates the local state on click.

Thus a Server component cannot have state itself, but it can make use of a Client component to maintain state and handle user interactions.

> Note that the `LikeButton` component is only updating the local like count and not the count in the server. The point of this example was to show that Client components can have state and user interaction.

## State Tree

To understand this, let's expand our example to have a `BlogPostList` Server component that renders a list of blogs using our `BlogPost` Server component.

```javascript
// BlogPost.server.js - A Server component.

import {getBlogPosts} from './blog/blog-api';
import BlogPost from './BlogPost.server';

export default function BlogPostsList() {
  const blogs = getBlogPosts();
  return (
    <>
      {blogs.map((blog) => (
        <BlogPost blog={blog} /> // Uses a server component.
      ))}
    </>
  );
}
```

Let's also update the `LikeButton` component to replace the state variable for `likes` with the `likes` from the props. Let's also add a callback function that hits the server to update the `likes` count of the particular blog post.

```javascript
// LikeButton.client.js - A Client component.

import {likeBlogPost} from './blog/blog-api';

import React from 'react';
import {useLocation} from './LocationContext.client'; // Experimental API for POC.

export default function LikeButton({blog}) {
  const [, setLocation] = useLocation();
  const handleClick = async () => {
    await likeBlogPost(blog.id);
    setLocation((loc) => ({
      ...loc,
      likes: blog.likes + 1,
    }));
  };

  const likeBlogPost = async (id) => {
    // Fetch call to update the blog post in the server.
  };

  return <span onClick={handleClick}>Likes: {blog.likes}</span>;
}
```

When you click on the like button, a call is made to the server to update the like count and then `setLocation` is called. This is an experimental API provided by the React team to mimic a call to the server to fetch a unit of the UI. In this case, we are fetching the component tree for the current route. You can see in the network tab that a call was indeed made and all the components in the current route beginning from the root are returned.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1lg73805zhw3o3hv6yub.gif)

The entire tree is rendered from the root and the parts that are updated are rendered, in this case, wherever `likes` is displayed on the screen. Note that the call to update was made from the `LikeButton` component however since the entire tree is updated the `likes` count passed as a `prop` to the `LikeButton` is updated.

## State of the Client components are maintained

Let's create a new `Comment` component, that renders an input text field bound to a state variable. For simplicity, we will not implement the comment functionality.

```javascript
// Comment.client.js - A Client component.

import React from 'react';

export default function Comment() {
  const [comment, setComment] = React.useState('');
  return (
    <input
      value={comment}
      onChange={({target: {value}}) => setComment(value)}
    />
  );
}
```

Type something in the comment text field of one of the blog posts. Now, click on any of the like buttons. You can see that even though the entire tree was rendered as a result of the like count update, the state of the Client components are preserved during such updates. As a result, whatever you typed in the comment box is intact and is not cleared. This is one of the biggest advantages of the Server components and a primary difference from traditional SSR.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lbnreshdq1gx5mracyfk.gif)

# Client Components

Client components are the components that we having been using all this while. But with server components in the mix, you need to remember one thing, 

> A Client component cannot import a server component however, it can render a server component passed in as children from a server component. Note that these props should be serializable, JSX is serializable and hence it can be passed in.

### Not Possible
```javascript
// FancyBlogPost.client.js - A Client component.
import React from 'react';
import BlogPost from './BlogPost.server';

export default function FancyBlogPost({ blog }) {
  return (
    <div className="fancyEffects">
      <BlogPost blog={blog} /> // Not OK. Cannot import a Server component inside a Client component.
    </div>
  );
}
```

```javascript
// BlogPostList.server.js - A Server component.
import {getBlogPosts} from './blog/blog-api';
import BlogPost from './BlogPost.server';

export default function BlogPostsList() {
  const blogs = getBlogPosts();
  return (
    <>
      {blogs.map((blog) => (
        <FancyBlogPost blog={blog}>
      ))}
    </>
  );
}
```

The reasoning is quite simple, Client components are sent to the client. If it were to contain a Server component accessing some internal API, that would fail in the client since it will not have access. This is just one reason among many.

Instead, we can do the following.

### Possible
```javascript
// FancyBlogPost.client.js - A Client component.
export default function FancyBlogPost({ children }) {
  return (
    <div className="fancyEffects">
      { children }
    </div>
  );
}
```

```javascript
// BlogPostList.server.js - A Server component.
export default function BlogPostsList() {
  const blogs = getBlogPosts();
  return (
    <>
      {blogs.map((blog) => (
        <FancyBlogPost>
          <BlogPost blog={blog} /> // Fine. Server component passed as childredn to a Client component.
        </FancyBlogPost>
      ))}
    </>
  );
}

```

This is fine because from the perspective of the Client component the content is already rendered in the server as part of the parent Server component and only the rendered content is passed as a `prop` to the Client component. 

Other things to remember with respect to Client components,

* They end with the extension `*.client.{js,jsx,ts,tsx}` (At least for now)
* They will be part of the client bundle and as such, you shouldn't be doing anything that you wouldn't want to be public. Eg: DB operations etc.
* They are free to use state and effect hooks.
* Use browser only API's.

# Shared Components
Shared components can be rendered either as a Server component or as a Client component. This is determined by what component imports it. Since it can be used either as a Server or a Client component it has the most limitations.

* They don't have a specific suffix.
* They cannot have `state`.
* They cannot make use of `useEffect` etc.
* They cannot render Server components.
* They cannot use browser specific API's.

With all these limitations, these components can only be used to display content that is passed as a prop to it.

# Thoughts and Conclusion
After reading this, if you are thinking that Server components are doing exactly what NextJS/SSR is doing. No. In the case of NextJS, the components are rendered in the server, yes, but eventually, the components are part of the client bundle and used for hydration. In addition, Server components allow for,
* Maintaining Client component state.
* A much granular integration of Client and Server components. For example, in NextJS, you are limited by pages to choose between client and server components.
* Code splitting is done based on file names and is now not an extra step to be done by the developers as an import.

Of course, there are parts that are being worked on like routing and stuff but I am genuinely excited by what Server components bring to the table. They provide the developers with the flexibility to choose between Client and Server components based on the requirements and get the best of both worlds. 

Hope, I was able to explain some of the concepts in a way that was easy to grasp. Happy coding and see you in the next one.. :)

Follow me on [Twitter](https://twitter.com/sidthesloth92) or check out my [website](https://dineshbalaji.in) to know more about me..✨