Using React Context in Combination With Server Components

Sjoerd Beentjes
4 min readNov 13, 2023

--

Source: Author with DALL-E 3

Server Components

React’s server components are an innovative addition to the React ecosystem, offering a new way to build and structure applications. They are rendered on the server, unlike traditional components which are rendered on the client. This distinction allows server components to access server-side resources directly, reduce the amount of code sent to the client, and improve overall performance.

The Problem with Context

One of the challenges with server components arises when dealing with hooks, such as ‘useContext’. In React, the Context API is a powerful tool for managing state across different components without prop-drilling. However, server components do not support the use of context in the same way as client components. The reason is that server components do not have a stable identity between renders, and their state cannot be preserved across different requests.

Combining Server Components and Client Components

To make React Context work with server components, we need to mix and match them smartly. Here’s a peek at how this works:

Context Example:

// ThemeProvider.tsx
import {
Dispatch,
PropsWithChildren,
SetStateAction,
createContext,
useContext,
useState,
} from 'react';

const ThemeContext = createContext<{
theme: string;
setTheme: Dispatch<SetStateAction<string>>;
}>({
theme: 'light',
setTheme: () => '',
});

export const ThemeProvider = ({
theme: initialTheme,
children,
}: PropsWithChildren<{ theme: string }>) => {
const [theme, setTheme] = useState(initialTheme);

console.log('ThemeProvider (on server & client)');

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div className={theme}>{children}</div>
</ThemeContext.Provider>
);
};

export const useTheme = () => {
return useContext(ThemeContext);
};

This bit of code sets up a theme state using a ThemeProvider. It's supposed to be available across the app.

Plugging it into layout:

// layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/ThemeProvider';
import './global.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<ThemeProvider theme="light">{children}</ThemeProvider>
</body>
</html>
);
}

We wrap the whole app layout in this ThemeProvider to make sure the theme is accessible everywhere.

Dealing with Server Component Restrictions

Running this setup throws up an error like this:

./components/ThemeProvider.tsx
ReactServerComponentsError:

You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Learn more: https://nextjs.org/docs/getting-started/react-essentials

,-[/home/projects/stackblitz-starters-pnhbhb/components/ThemeProvider.tsx:5:1]
5 | SetStateAction,
6 | createContext,
7 | useContext,
8 | useState,
: ^^^^^^^^
9 | } from 'react';
10 |
11 | const ThemeContext = createContext<{
`----

Maybe one of these should be marked as a client entry with "use client":
./components/ThemeProvider.tsx
./app/layout.tsx

his happens because server components can’t use state-managing hooks like useState. The fix? Mark your component as a client component with "use client".

// ThemeProvider.tsx
'use client' // <- add this
import {
Dispatch,
PropsWithChildren,
SetStateAction,
createContext,
useContext,
useState,
} from 'react';

const ThemeContext = createContext<{
theme: string;
setTheme: Dispatch<SetStateAction<string>>;
}>({
theme: 'light',
setTheme: () => '',
});

export const ThemeProvider = ({
theme: initialTheme,
children,
}: PropsWithChildren<{ theme: string }>) => {
const [theme, setTheme] = useState(initialTheme);

console.log('ThemeProvider (on server & client)');

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div className={theme}>{children}</div>
</ThemeContext.Provider>
);
};

export const useTheme = () => {
return useContext(ThemeContext);
};

The thing that I until here didn’t realise is that the children of this component will not automatically be rendered client components. In fact they will be rendered on the server, unless otherwise specified using a “use client” hint.

Example of how the example is rendered

The Catch

Here’s the catch: any component that wants to use the theme state also has to be a client component. The thing to remember is that if a component needs to use the theme state, it has to be a client component. This isn’t necessarily a bad thing; it just changes how the component behaves. Marking a component as a client component allows you to use tools like state and context, which are super handy. But, it also means that component can’t directly access server-side resources like a server component can. So, while it gives you some neat features, you also lose out on some server-side perks. It’s about deciding what’s more important for each part of your app: client-side interactivity or server-side efficiency.

Here’s a full example to show you how it all works together, mixing server and client components with React Context:

--

--

Sjoerd Beentjes

A developer from the Netherlands. My articles are like notes, when I learn something new I try to write about it.