skip to content
Nikolas Barwicki - Javascript Blog Nikolas's Blog

Are React Client Components All Bad? A Look At Next.js 13

/ 8 min read

There’s a common misconception that React Client Components are fundamentally flawed and should be avoided.

However, this couldn’t be further from the truth.

In this article, we’ll explore React Server Components in Next.js 13, and uncover the value of using React Client Components alongside them for a high-performance, user-friendly application.

Understanding React Client Components

Contrary to popular belief, there’s nothing wrong with using React Client Components.

These components are designed to add client-side interactivity, as they’re pre-rendered on the server and hydrated on the client. React Client Components, or RCCs, work the same way as components in the Pages Router have always worked. With React Server Components (RSC), we’re simply saying that certain components don’t need the hydration step for interactivity, as they’re purely visual.

React Client components maintain their functionality, still being server-side rendered (SSR) to HTML. In other words, client components are the components you’ve always known, having been SSR’d before to improve first load performance.

The terms “Server” and “Client” don’t directly correspond to a physical server and client. Instead, you can think of them as “React Server” and “React Client”. Props flow from React Server to React Client, with a serialization boundary separating them. The React Server typically runs at build time or on an actual server, while React Client runs in multiple environments, managing the DOM in browsers and generating initial HTML elsewhere.

The Role of React Server Components

In Next.js 13, all components inside the App Router are Server Components by default, including special files and colocated components. This means we can automatically adopt Server Components without any extra effort, resulting in great performance out of the box.

One key feature of React Server Components is Streaming and Suspense. These new React features allow you to progressively render and incrementally stream rendered UI units to the client. With Server Components and nested layouts, your application can instantly render data-independent parts of the page and display a loading state for data-dependent sections.

As a result, users won’t have to wait for the entire page to load before interacting with it.

What is Streaming?

To understand Streaming in React and Next.js, let’s first grasp the concept of server-side rendering (SSR) and its limitations. With SSR, a series of sequential, blocking steps need to be completed before a user can see and interact with a page:

  1. Fetch data on the server.
  2. Render HTML on the server.
  3. Send HTML, CSS, and JavaScript files to the client.
  4. Display a non-interactive UI using the generated HTML and CSS.
  5. Hydrate the UI with React to make it interactive.

While SSR with React and Next.js does improve perceived loading performance, issues may still arise, such as slow data fetching on the server. That’s where Streaming comes in, allowing for the incremental delivery of page HTML to the client in smaller chunks. This feature can display parts of the page more quickly, removing the need to wait for all data to load before rendering any UI.

Streaming works well with React’s component model, as each component can be considered a chunk. High-priority or non-data-reliant components can be sent first, with React beginning hydration sooner. Lower-priority components can be sent in the same server request after their data has been fetched. This concept is particularly beneficial when developers want to avoid long data requests from blocking page rendering, as Streaming can reduce time to first byte (TTFB), first contentful paint (FCP), and time to interactive (TTI).

Streaming with Suspense

React’s <Suspense> component works by wrapping a component performing asynchronous actions, such as fetching data, and displaying fallback UI while awaiting completion. Once that action completes, the component will replace the fallback UI.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'

export default function Posts() {
  return (
    <section>
      <Suspense fallback={<p>Loading feed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  )
}

By utilizing Suspense, your application benefits from Streaming Server Rendering and Selective Hydration based on user interaction.

When to Use Server and Client Components

In essence, the more you use React Server Components (RSC), the more performant and user-friendly your application will become. This is due to granular loading states and the elimination of classic SSR problems. However, there are various use cases for both Server and Client components:

Server components use cases

1. Fetch data

When you need to fetch data, Server Components can efficiently handle the task. The following example demonstrates the use of an async function to fetch the data and render the page.

// app/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/...')

  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }

  return res.json()
}

export default async function Page() {
  const data = await getData()

  return <main>{data}</main>
}

2. Access backend resources (directly)

Server Components can directly access backend resources. In this example, we’re using Prisma to fetch data from a database.

import prisma from '@/lib/prisma'

async function getPosts() {
  const posts = await prisma.post.findMany()
  return posts
}

export default async function Page() {
  const posts = await getPosts()
  // ...
}

3. Keep sensitive information on the server (access tokens, API keys, etc)

Server Components provide a secure way to store sensitive information, like API keys or access tokens, on the server-side.

4. Keep large dependencies on the server / Reduce client-side JavaScript

By using Server Components, you can reduce the bundle size by keeping large dependencies on the server-side.

// *without* Server Components

import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

This example demonstrates how marking and sanitizing HTML in a Server Component doesn’t add to the client bundle size.

// *with* Server Components === zero bundle size

import marked from 'marked' // zero bundle size
import sanitizeHtml from 'sanitize-html' // zero bundle size

function NoteWithMarkdown({ text }) {
  // same as before
}

Client components use cases

1. Add interactivity and event listeners (onClick(), onChange(), etc):

Client Components are valuable when you need to add interactivity, such as click or change events. The following example demonstrates a simple counter component that increments when clicked.

'use client'

import React, { useState } from 'react'

export default function MyComponent() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(count + 1)
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  )
}

2. Use State and Lifecycle Effects (useState(), useReducer(), useEffect(), etc):

To manage state and implement lifecycle effects, Client Components demonstrate their utility through the useState and useEffect React hooks. The following Timer component shows the elapsed time since it was mounted.

'use client'

import React, { useState, useEffect } from 'react'

export default function Timer() {
  const [seconds, setSeconds] = useState(0)

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds((prevSeconds) => prevSeconds + 1)
    }, 1000)

    return () => {
      clearInterval(intervalId)
    }
  }, [])

  return <div>Seconds: {seconds}</div>
}

3. Use browser-only APIs:

Some APIs are only available within the browser environment. Client Components are the ideal place to access browser-only APIs like the Geolocation API in this example.

'use client'

import React, { useEffect } from 'react'

export default function GeolocationComponent() {
  useEffect(() => {
    navigator.geolocation.getCurrentPosition((position) => {
      const { latitude, longitude } = position.coords
      console.log(`Latitude: ${latitude}, Longitude: ${longitude}`)
    })
  }, [])

  return <div>Geolocation component</div>
}

4. Use custom hooks that depend on state, effects, or browser-only APIs:

Custom hooks can depend on state, effects, or browser-only APIs but still function within Client Components. The following example demonstrates how to create an online status component that tracks whether the user is online or offline.

'use client'

import React, { useState, useEffect } from 'react'

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine)

  useEffect(() => {
    const handleOnline = () => setIsOnline(true)
    const handleOffline = () => setIsOnline(false)

    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOffline)

    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOffline)
    }
  }, [])

  return isOnline
}

export default function OnlineStatusComponent() {
  const isOnline = useOnlineStatus()

  return (
    <div>
      <p>Status: {isOnline ? 'Online' : 'Offline'}</p>
    </div>
  )
}

By understanding when to use Server and Client components, you can harness their full potential to create a seamless, high-performance Next.js application.

Optimizing Performance with Server and Client Components

When combining Server and Client Components in your Next.js application, you may use the following patterns to enhance your app’s performance:

  1. Moving Client Components to the Leaves: By relocating Client Components to the outermost portions of your component tree, greater performance is achieved. This reduces the amount of JavaScript sent to the client side, ensuring only smaller interactive components are transmitted.
  2. Nesting Server Components inside Client Components: To prevent the need for additional server round-trips, require your Client Components to accept React props, then pass Server Components as props to them. This separates the rendering process and allows each component to operate independently, aligning with the Server Components’ design.

By carefully employing these performance optimization techniques, you can build fast, responsive applications that prioritize user experience without compromising on functionality.

Say goodbye to the false notion that React Client Components are inherently bad, and start using both Client and Server components to power your applications.

Summary

In this article, we debunk the misconception that React Client Components should be avoided and explore the benefits of using them alongside React Server Components in Next.js 13. We delve into the integration of React Server Components and React Client Components, highlighting their roles and how they work together to create a high-performance and user-friendly application.

We also discuss the use cases for both Server and Client components, providing examples and code snippets to illustrate their functionality. Additionally, we explore optimization techniques such as moving Client Components to the leaves and nesting Server Components inside Client Components to enhance overall performance. By leveraging the power of React Client Components and React Server Components, developers can build fast and responsive applications that prioritize user experience.