React Server Components(RSC)

When building applications with Next.js, it's important to grasp the concepts of Server Components and Client Components. These components allow developers to create applications that combine the interactivity of client-side apps with the performance benefits of server rendering.

react comoponents image

Server Components: Leveraging Server Infrastructure

Server Components offer several advantages over Client Components. They enable developers to leverage server infrastructure more effectively. By moving data fetching closer to the server and keeping large dependencies on the server, Server Components improve overall performance. They provide the flexibility to write React applications that feel akin to frameworks like PHP or Ruby on Rails, while still benefiting from the power and flexibility of React's component model for UI templating.

With Server Components, the initial page load is faster, and the client-side JavaScript bundle size is reduced. The base client-side runtime remains cacheable and predictable in size, regardless of application growth. Additional JavaScript is only added when client-side interactivity, through the use of Client Components, is required.

Client Components: Adding Interactivity

Client Components allow developers to incorporate client-side interactivity into their applications. In Next.js, these components are pre-rendered on the server and then hydrated on the client side. You can think of Client Components as similar to components in the Pages Router, providing a familiar approach to building interactive UI elements.

The "use client" Directive: Defining the Component Boundary

The "use client" directive serves as a convention for declaring the boundary between Server and Client Components. Placed at the top of a file, above imports, it marks the transition point from server-only to client code. Once defined in a file, all modules imported into it, including child components, become part of the client bundle.

It's important to note the following regarding the "use client" directive:

  • Components in the Server Component module graph are rendered exclusively on the server.
  • Components in the Client Component module graph are primarily rendered on the client side, but Next.js allows them to be pre-rendered on the server and hydrated on the client.
  • The "use client" directive must be defined at the top of a file before any imports.
  • It only needs to be defined once, typically at the entry point of your application, to encompass all subsequently imported modules as Client Components.
'use client'

import { useState } from 'react'

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}
useclient image

Choosing Between Server and Client Components

To simplify the decision-making process, it is recommended to use Server Components (the default in the app directory) unless there is a specific use case for a Client Component. Server Components offer benefits such as improved performance, reduced client-side JavaScript, and optimized server infrastructure utilization.

Patterns: Moving and Composing Components

Two patterns worth noting are moving Client Components to the leaves of the component tree and composing Server and Client Components.

Moving Client Components to the leaves involves separating interactive logic from static elements, keeping the layout as a Server Component and moving the interactive logic to a Client Component. This optimization reduces the amount of component JavaScript sent to the client.

Composing Server and Client Components allows you to combine both types of components within the same component tree. React handles the rendering process, merging the server and client work seamlessly to produce a faster initial page load.

It's important to be aware of the restriction on importing Server Components into Client Components. This pattern requires an additional server round trip and is not supported.


Good to know: In Next.js, during the initial page load, both the rendered result of Server Components from the above step and Client Components are pre-rendered on the server as HTML to produce a faster initial page load.


Unsupported Pattern: Importing Server Components into Client Components

The following pattern is not supported. You cannot import a Server Component into a Client Component:

'use client'

// This pattern will **not** work!
// You cannot import a Server Component into a Client Component.
import ExampleServerComponent from './example-server-component'

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      <ExampleServerComponent />
    </>
  )
}

Recommended Pattern: Passing Server Components to Client Components as Props

Instead, when designing Client Components you can use React props to mark "holes" for Server Components. The Server Component will be rendered on the server, and when the Client Component is rendered on the client, the "hole" will be filled in with the rendered result of the Server Component.

A common pattern is to use the React children prop to create the "hole". We can refactor <ExampleClientComponent> to accept a generic children prop and move the import and explicit nesting of <ExampleServerComponent> up to a parent component.

'use client'

import { useState } from 'react'

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      {children}
    </>
  )
}

Now, <ExampleClientComponent> has no knowledge of what children is. In fact, from its perspective, it doesn't even know that children will eventually be filled in by the result of a Server Component. The only responsibility ExampleClientComponent has is to decide where whatever children will eventually be should be placed.

In a parent Server Component, you can import both the <ExampleClientComponent> and <ExampleServerComponent> and pass <ExampleServerComponent> as a child of <ExampleClientComponent>:

// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ExampleClientComponent from './example-client-component'
import ExampleServerComponent from './example-server-component'

// Pages in Next.js are Server Components by default
export default function Page() {
  return (
    <ExampleClientComponent>
      <ExampleServerComponent />
    </ExampleClientComponent>
  )
}

With this approach, the rendering of <ExampleClientComponent> and <ExampleServerComponent> are decoupled and can be rendered independently - aligning with Server Components, which are rendered on the server before Client Components.

Good to know

  • This pattern is already applied in layouts and pages with the children prop, so you don't have to create an additional wrapper component.
  • Passing React components (JSX) to other components is not a new concept and has always been part of the React composition model.
  • This composition strategy works across Server and Client Components because the component that receives the prop has no knowledge of what the prop is. It is only responsible for where the thing that it is passed should be placed.
  • This allows the passed prop to be rendered independently, in this case, on the server, well before the Client Component is rendered on the client.
  • The very same strategy of "lifting content up" has been used to avoid state changes in a parent component re-rendering an imported nested child component.
  • You're not limited to the children prop. You can use any prop to pass JSX.
  • Passing props from Server to Client Components (Serialization)
  • Props passed from the Server to Client Components need to be serializable. This means that values such as functions, Dates, etc, cannot be passed directly to Client Components.

Where is the Network Boundary?

In the App Router, the network boundary is between Server Components and Client Components. This is different from the Pages where the boundary is between getStaticProps/getServerSideProps and Page Components. Data fetched inside Server Components do not need to be serialized as it doesn't cross the network boundary unless it is passed to a Client Component. Learn more about data fetching with Server Components.

Keeping Server-Only Code out of Client Components (Poisoning)

Since JavaScript modules can be shared between both Server and Client Components, it's possible for code that was only ever intended to be run on the server to sneak its way into the client.

For example, take the following data-fetching function:

// lib/data.ts

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

At first glance, it appears that getData works on both the server and the client. But because the environment variable API_KEY is not prefixed with NEXT_PUBLIC, it's a private variable that can only be accessed on the server. Next.js replaces private environment variables with the empty string in client code to prevent leaking secure information.

As a result, even though getData() can be imported and executed on the client, it won't work as expected. And while making the variable public would make the function work on the client, it would leak sensitive information.

So, this function was written with the intention that it would only ever be executed on the server.

The "server only" package To prevent this sort of unintended client usage of server code, we can use the server-only package to give other developers a build-time error if they ever accidentally import one of these modules into a Client Component.

To use server-only, first install the package:

npm install server-only

Then import the package into any module that contains server-only code:

// lib/data.js

import 'server-only'

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })

  return res.json()
}

Now, any Client Component that imports getData() will receive a build-time error explaining that this module can only be used on the server.

The corresponding package client-only can be used to mark modules that contain client-only code – for example, code that accesses the window object.

Third-Party Packages and Compatibility with Server Components Not all third-party packages have been updated to support Server Components. Some packages may rely on client-only features and lack the "use client" directive required for Server Components. In such cases, these packages won't work within Server Components.

To address this, you can create wrapper components for third-party packages, marking them as Client Components and enabling their usage within Server Components.

As the ecosystem around Server Components continues to evolve, more third-party packages are expected to incorporate the "use client" directive, providing seamless support for Server Components.

Note: The code examples provided are for illustrative purposes and may not represent the exact implementation in your project. Please adapt the code to your specific use case.

Using Context in Client Components

Context is a powerful feature in React that allows for sharing data between components. In Next.js 13, context is fully supported within Client Components. You can create context using createContext and consume it using useContext. For example:

'use client'
 
import { createContext, useContext, useState } from 'react'
 
const SidebarContext = createContext()
 
export function Sidebar() {
  const [isOpen, setIsOpen] = useState()
 
  return (
    <SidebarContext.Provider value={{ isOpen }}>
      <SidebarNav />
    </SidebarContext.Provider>
  )
}
function SidebarNav() {
let { isOpen } = useContext(SidebarContext)

return (
  <div>
    <p>Home</p>

    {isOpen && <Subnav />}
  </div>
)
}

Rendering Context Providers in Server Components

Creating context directly at the root level in Server Components is not supported. To overcome this, you can create your context within a Client Component and render it in your Server Component. For example:

// theme-provider.tsx
'use client'
 
import { createContext } from 'react'
 
export const ThemeContext = createContext({})
 
export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
// layout.tsx
import ThemeProvider from './theme-provider'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
  <html>
    <body>
      <ThemeProvider>{children}</ThemeProvider>
    </body>
  </html>
)
}

This way, you can consume the context in other Client Components throughout your application.

Rendering Third-Party Context Providers in Server Components

Rendering third-party context providers directly in Server Components may cause errors if they haven't implemented support for Server Components. To resolve this, you can wrap third-party providers in your own Client Component and render it in your Server Component. For example:

// providers.js
'use client'
 
import { ThemeProvider } from 'acme-theme'
import { AuthProvider } from 'acme-auth'
 
export function Providers({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>{children}</AuthProvider>
    </ThemeProvider>
  )
}
// layout.js
import { Providers } from './providers'
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

This ensures that the third-party providers work as expected within your Server Components.

Sharing Data Between Server Components

Since Server Components are not interactive and do not rely on React state, you don't need React context to share data between them. Instead, you can use native JavaScript patterns for data sharing. For example, you can create a module to share a database connection across multiple components:

// utils/database.ts
export const db = new DatabaseConnection()

Then, you can import and use this module in your Server Components:

// users/layout.tsx
import { db } from '@utils/database'
 
export async function UsersLayout() {
  let users = await db.query()
  // ...
}

// users/[id]/page.tsx
import { db } from '@utils/database'
 
export async function DashboardPage() {
  let user = await db.query()
  // ...
}

By importing the @utils/databasemodule, both components can access the shared database connection. This approach is known as global singletons.

Sharing Fetch Requests Between Server Components When fetching data in Server Components, you may want to share the fetched result between a page or layout and its child components. Instead of passing props back and forth, it's recommended to colocate data fetching alongside the component that consumes the data. Next.js automatically deduplicates fetch requests in Server Components, so each component can request the specific data it needs without worrying about duplicate requests. Next.js will read the same value from the fetch cache.

Note: the code examples provided are for illustration purposes and may require adaptation to fit your specific use case.

Author

References

Remember to consult the official Nextjs documentation for the most up-to-date information on React.