souporserious

Managing React Context in Server Components

5 min read

React Server Components introduce a new way to build React applications. This post will look at how to manage Context in this latest paradigm.

The first public version of React Server Components recently made its debut at NextJS Conf 2022. This new concept dramatically simplifies how data is fetched in React components and trims them down to only include the static code collected on the server:

1export default async function App() {
2 const response = await fetch('https://pokeapi.co/api/v2/pokemon')
3 const allPokemon = (await response.json()).results
4
5 return (
6 <ul>
7 {allPokemon.map((pokemon) => (
8 <li key={pokemon.name}>{pokemon.name}</li>
9 ))}
10 </ul>
11 )
12}

While most everything you’re used to when developing React applications is the same, Server Components have a few new constraints. Specifically, we can no longer rely on interactivity; this includes the standard React state hooks you’re used to, like useState, useContext, and event handlers like onClick.

When you need to introduce interactivity, you do so by marking the top of the file with a use client directive. This directive informs the bundler you’re using that the component requires additional JavaScript to run beyond the server:

1'use client'
2import { useState } from 'react'
3
4export function Counter() {
5 const [count, setCount] = useState(0)
6
7 return (
8 <div>
9 <button onClick={() => setCount((count) => count - 1)}>Decrease</button>
10 <span>{count}</span>
11 <button onClick={() => setCount((count) => count + 1)}>Increase</button>
12 </div>
13 )
14}

See the list in the NextJS docs for a detailed guide on when to use Server vs. Client components.

Managing Context

With these new APIs explained, let’s look at how we can weave React Context throughout our application. We’ll use a simple NextJS 13 application that defines a SystemProvider, allowing users of our library to specify a preferred font size. Follow along at the Codesandbox below:

Edit Managing React Context in Server Components

To keep this post focused, we won’t get into the details of how the new layout architecture works in NextJS. Please refer to the documentation for a more in-depth overview.

Leaning Into Composition

Server Components will push developers to use useful JSX features that have always been available, like the children prop or any prop that returns JSX elements. This feature is excellent because props that return JSX allow consumers of your components to compose building blocks together rather than you having to make too many assumptions about how your components work together.

In our app directory, we’ll use the standard layout that NextJS ships with when using the automatic installation:

layout.tsx.tsx
1import './globals.css'
2
3export default function RootLayout({
4 children,
5}: {
6 children: React.ReactNode
7}) {
8 return (
9 <html lang="en">
10 <head>
11 <title>Create Next App</title>
12 <meta name="description" content="Generated by create next app" />
13 <link rel="icon" href="/favicon.ico" />
14 </head>
15 <body>{children}</body>
16 </html>
17 )
18}

If we try to import React.createContext here, we will get an error; remember, Server Components will only return stringified content and cannot contain additional JavaScript. Thankfully, the NextJS team has helped us here and will throw an eslint error when trying to do this. Now is where we will create our first Client Component. Let’s make a new SystemProvider file to house our Context that allows users to control the font size:

components/SystemProvider.tsx.tsx
1'use client'
2import { createContext, useContext } from 'react'
3
4const SystemContext = createContext<{
5 fontSize: 'small' | 'medium' | 'large'
6}>({
7 fontSize: 'medium',
8})
9
10export function useSystem() {
11 return useContext(SystemContext)
12}
13
14export function SystemProvider({
15 children,
16 fontSize,
17}: {
18 children: React.ReactNode
19 fontSize: 'small' | 'medium' | 'large'
20}) {
21 return (
22 <SystemContext.Provider value={{ fontSize }}>
23 {children}
24 </SystemContext.Provider>
25 )
26}

Next, we’ll add a Text component that consumes this Context. Again, this will be a Client Component because of the reliance on useContext. As a good rule for now, anytime you need a hook, you will most likely need to mark that component utilizing that hook as a Client Component:

components/Text.tsx.tsx
1'use client'
2import { useSystem } from './SystemProvider'
3
4const fontSizes = {
5 small: '0.8rem',
6 medium: '1rem',
7 large: '1.2rem',
8}
9
10export function Text({
11 href,
12 children,
13 ...restProps
14}: {
15 href?: string
16 children: React.ReactNode
17} & React.HTMLAttributes<HTMLElement>) {
18 const Element = href ? 'a' : 'span'
19 const { fontSize } = useSystem()
20
21 return (
22 <Element
23 href={href}
24 style={{ fontSize: fontSizes[fontSize] }}
25 {...restProps}
26 >
27 {children}
28 </Element>
29 )
30}

Now, we can use both components throughout our application and interweave them into our Server Components. We’ll add the SystemProvider to our top-level layout:

layout.tsx.tsx
1export default function RootLayout({ children }) {
2 return (
3 ...
4 <body>
5 <SystemProvider fontSize="large">{children}</SystemProvider>
6 </body>
7 ...
8 )
9}

We explicitly set a static font size that showcases the power of Server and Client components. We can pass serializable data between these components while retaining the benefits of only loading what’s needed. In a real-world application or as an exercise for the reader, you will likely have another Client Component that offers a toggle that allows updating the Provider value using your flavor of state management.

Conclusion

This was a simple look at the constraints of using Context in the new world of Server Components. Remember, anytime you need to add functionality that will update beyond static content, you can start to introduce Client Components. A nice benefit of this new constraint is it draws a clear boundary of what should and should not live in Context. In most cases, Context is most beneficial for scenarios like user preferences, but this also begs the question of where that state should live. It could all move to the server now, depending on what you are building.

Updated:
  • react
  • context
  • server
  • components
  • nextjs
Previous post
Build Your Own Code Playground
© 2022 Travis Arnold