Managing React Context in Server Components
5 min readReact 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()).results45 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'34export function Counter() {5 const [count, setCount] = useState(0)67 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:
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:
1import './globals.css'23export default function RootLayout({4 children,5}: {6 children: React.ReactNode7}) {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:
1'use client'2import { createContext, useContext } from 'react'34const SystemContext = createContext<{5 fontSize: 'small' | 'medium' | 'large'6}>({7 fontSize: 'medium',8})910export function useSystem() {11 return useContext(SystemContext)12}1314export function SystemProvider({15 children,16 fontSize,17}: {18 children: React.ReactNode19 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:
1'use client'2import { useSystem } from './SystemProvider'34const fontSizes = {5 small: '0.8rem',6 medium: '1rem',7 large: '1.2rem',8}910export function Text({11 href,12 children,13 ...restProps14}: {15 href?: string16 children: React.ReactNode17} & React.HTMLAttributes<HTMLElement>) {18 const Element = href ? 'a' : 'span'19 const { fontSize } = useSystem()2021 return (22 <Element23 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:
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.
- react
- context
- server
- components
- nextjs