Web Dev Simplified Blog

React Server Components - A New Paradigm

November 27, 2023

It has been quite a few years since React released hooks and over that time opinions have changed from super excited to extremley frustrated. Developers are becoming more and more frustrated with React hooks and that is almost entirely due to fetching data and more specifically the useEffect hook.

Before React fetching data was a simple process you did on the server before rendering your pages, but with React pushing everything to the client this data fetching became infinitely more complex since you now had additional error and loading states to deal with. This is where server components come in. Server components are a new way of writing React applications and drastically simplifies the painful process of fetching data while retaining all the benefits of React.

What Are Server Components?

The big thing that makes server component different than client components is that server components never render on the client and instead only render on the server. This may sound like a downside since these components are less flexible, but since these components never touch the client it makes data fetching a breeze. Here is a comparison of a client component and a server component.

function ClientComponent() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(false)

  useEffect(() => {
    setLoading(true)
    setData(undefined)
    setError(false)

    const controller = new AbortController()

    fetch("/api/data", { signal: controller.signal })
      .then(res => res.json())
      .then(data => setData(data))
      .catch(error => setError(true))

    return () => controller.abort()
  }, [])

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error</p>

  return <p>{data}</p>
}
async function ServerComponent() {
  try {
    const data = await fetch("/api/data").then(res => res.json())
    return <p>{data}</p>
  } catch (error) {
    return <p>Error</p>
  }
}

Even in this very simple example where the client component is fetching data in the simplest way possible you still have to write tons of complex React specific code. With server components, though, this code is much simpler to understand and write since it works like normal JavaScript code.

The reason this code can be written so simply is because server components are rendered entirely on the server and the HTML output of that component is sent to the client as plain HTML. This means that when a user requests a page that renders a server component they will be sent down the HTML of that server component after it has already been rendered on the server. That HTML is then hydrated on the client and React takes over from there.

Benefits Of Server Components

Server components are an incredible tool that makes data fetching much easier as we have already talked about, but there are tons of other amazing benefits that come with server components.

Security

Since server components are run entirely on the server you can safely use secret API keys or fetch data directly from a database since the client will never see this code.

async function ServerComponent() {
  const user = await db.users.find({ name: "John" })

  return <p>{user.name}</p>
}

This may sound like a minor bonus, but this is actually a huge deal since it means you don’t need to create a seperate public API just to power your React application. You can just make all your database calls directly from your server components.

Performance

There are actually multiple ways that server components increase the performance of your application.

Caching

Since all your code is run on the server it means you can cache data between requests and between users. For example, if you have a list of blog articles that you know doesn’t change often you can cache that list of articles and serve it to every user without having to fetch it from the database every time.

async function ServerComponent() {
  // Cache for 1 minute across all requests
  const articles = await cache(db.articles.findAll, { timeSec: 60 })

  return <p>{articles.length}</p>
}

Bundle Size

Another huge benefit is that since server components are rendered on the server you don’t need to send any JS code to the client for those components. This drastically reduces the bundle size of your application and makes it much faster to load.

Page Load Speeds

Since server components are rendered on the server it means that the HTML for those components is sent to the client as soon as the user requests the page. This means that the user will see the content of the page much faster than if you were using client components since the client components need to wait for the JS to load before they can render. This is very important for SEO since Google highly values page load speeds.

SEO

Speaking of SEO, server components are also much better for SEO since the HTML for those components is sent to the client immediatley without having to wait for JS to load. This helps search engines index your pages and makes them easier to find on Google.

Downsides Of Server Components

Just like eveything in life if it sounds too good to be true it probably is. Server components are no exception and there are a few downsides with using them.

No Interactivity

Since server components are rendered on the server it is impossible for you to have any interactivity in your server components. This means you cannot use React hooks, event listeners, or any other code that involves user interaction.

This may sound like a deal breaker, but since you can easily nest normal client components inside server components this is really not a problem.

async function ServerComponent() {
  const articles = await db.articles.findAll()

  return articles.map(article => <Article article={article} />)
}
"use client"

// This is a normal client component so you can use hooks and event listeners
function Article() {
  const [likes, setLikes] = useState(0)

  return (
    <div>
      <p>{article.title}</p>
      <p>{article.content}</p>
      <button onClick={() => setLikes(l => l + 1)}>Like</button>
      <p>{likes}</p>
    </div>
  )
}

As you can see in the above code we are able to use server components to fetch our data and then use client components to add interactivity to that data. This is a very powerful pattern and makes it much easier to write complex applications.

No Browser APIs

Another downside is you cannot use any browser APIs in server components since they are rendered on the server and have no access to client APIs. This means things like localStorage, navigator, and window are all unavailable in server components. This again isn’t a major problem since you can just use nested client components to access these APIs.

Cannot Easily Be Nested In Client Components

Server components cannot be nested inside clients components. This is somewhat obvious as a client component is rendered on the client so you cannot also have a server component inside of it.

function ServerComponent() {
  return <p>Hello Server</p>
}
"use client"

function ClientComponent() {
  return (
    <div>
      <p>Hello Client</p>
      {/* This will not work */}
      <ServerComponent />
    </div>
  )
}

You can partially get around this problem (at least in Next.js) by passing the server component as a prop to the client component and then rendering it inside the client component.

function ServerComponent() {
  return <p>Hello Server</p>
}
"use client"

function ClientComponent({ children }) {
  return (
    <div>
      <p>Hello Client</p>
      {children}
    </div>
  )
}
function AnotherServerComponent() {
  return (
    <ClientComponent>
      {/* This will work */}
      <ServerComponent />
    </ClientComponent>
  )
}

For the most part this is not often something you need to do, but if you find yourself in this situation this is a good workaround.

How To Use Server Components

Server components are still very experimental and thus are not able to be used directly inside of React. Instead you must use a framework that implements server components to use them. Currently the only framework that implements server components is Next.js, but many other frameworks are experimenting with implementing server components.

In Next.js specifically all components you create are server components by default. In order to use a client component you must opt into it by adding the "use client" string at the top of the file.

"use client"

// This is a client component
function ClientComponent() {
  return <p>Hello Client</p>
}

The reason for this server first approach is because server components are almost always better to use unless you specifically need interactivity or browser APIs.

Conclusion

Server components are a new way of writing React applications that make it easier than ever to create complex applications since they solve many of the complexities around data fetching and performance. Although, Server components are still very experimental, frameworks like Next.js make server components accessible in a stable way.