Using Context and State in Next.js Server Components

·

3 min read

Introduction: The Problem at Hand

The Context API allows sharing values across the component tree without explicitly passing them as props. However, the introduction of React's Server Components, has thrown a wrench into the mix. Server components promise better performance by allowing components to be rendered on the server-side, thereby sending only the minimal required HTML and data to the client. But this architectural shift presents a challenge: the server does not inherently manage or understand 'state' as the client does. Hence, React's server components do not natively support context.

The Conceptual Rundown

Why State on the Server?

Servers are stateless by nature, meaning they treat each request as an isolated transaction, unrelated to any previous or future requests. This makes them efficient and scalable. But when we talk about state in server components, we're referring to the idea of sending pre-rendered components that have some awareness of the data or environment they are rendered within.

Client Component and Context

React's Client Components maintain the capability to manage and respond to state changes. This is vital for interactive elements of a web application, as they require immediate feedback without a server roundtrip. It is this capability that allows Client Components to harness the power of the Context API.

Server Components: Stateless By Design

Server Components, due to their stateless nature, do not directly support the Context API. This is a deliberate design choice, ensuring server-rendered components remain as lightweight and performant as possible. Rendering context at the root of your server component would break this statelessness.

Theory Will Only Take You So Far: A Code Walkthrough

To understand this in a practical scenario, let's consider implementing theming for our React application. In a traditional setup, a theme provider would be rendered at the root of the application, potentially using hooks like useState to toggle between themes. Now, in the context of server components, if we try to directly render a theme provider at the root and introduce something like useState, we face a dilemma. You can't simply import a server-rendered component and nest it within this theme provider, as useState requires a client-side setting.

So, how do we solve this problem?

The solution lies in separating concerns. First, we encapsulate our theme context and state within a client component. This ensures that the theming, with its inherent stateful logic, resides solely on the client side. Then, this client component can be integrated into the broader server component framework, allowing for server actions or components to be nested as its children.

1. Creation of the Client Component with Context

// 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>
}

Here, the 'use client' declaration instructs React to treat ThemeProvider as a client-side component. This enables the component to use context and any stateful logic we may introduce later.

2. Incorporating Client Component into Server Component

// layout.tsx

import ThemeProvider from './theme-provider'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

With this architecture, the ThemeProvider, being a client-side component, wraps around the server-rendered components inside RootLayout. The server recognizes ThemeProvider's client-side nature and ensures stateful logic is executed correctly, while still leveraging the benefits of server-rendered components for the rest of the application.