Leveraging Server Context for Advanced Next.js Optimization Patterns
During the last decade, the JavaScript ecosystem has been dominated by Single Page Applications (SPA). SPAs rely extensively on client-side JavaScript that runs and renders within the user’s browser. It’s great for interactive applications with rich user interfaces.
Rendering on the server has, however, made an impressive comeback, thanks to frameworks such as Gatsby, Next, and Astro. Server-side rendering offloads computation from the user's computer and can be used to improve performance in various situations.
You need to understand server-side rendering if you plan to implement content-heavy applications such as e-commerce, blogs, or even some SaaS products.
Yet, server-side rendering edge cases can be so tricky that I am not sure that, at this point, anyone has it completely figured out. Otherwise, we wouldn’t have three new JavaScript frameworks a week, am I right? This is especially true when you couple server-side rendering with highly interactive technologies such as React.
Last year, as I wrote and published a few papers on the relationship between static rendering (SSG) and per-request server-side rendering (SSR), I observed that most users won’t have to worry about the most complex use cases. Yet if they plan to implement some advanced features, they might hit a wall, and it will be difficult to understand why. What’s behind the wall? Server context.
In this article, I’d like to focus on the concept of “server context.” We will learn why accessing this context or extending it is very important, and how it could be done in practice, through the example of Next.js 13’s new methods: “headers()”, “cookies()”, patched fetch() and React 18 cache.
These insights should help you to better understand the internals of Next.js and how you can leverage them to implement advanced optimization patterns.
Server rendering is all about requests
Server-Side Rendering (SSR) is about taking user requests as input and producing HTML, CSS, and JavaScript as output. Static rendering (sometimes dubbed SSG for Static Site Generation) is very similar, except that it happens at build time before requests start hitting the server.
If this wording sounds confusing, it’s because it is confusing. Both happen on the server, but “SSR” means specifically per-request rendering, as opposed to build-time rendering. Next.js 13 proposes a much clearer terminology:
- Dynamic rendering happens server-side, at request time. This used to be called “SSR.”
- Static rendering happens server-side, too, but at build-time. This used to be called “SSG.”
So the difference lies in the fact that static rendering doesn’t take a request as input, while dynamic rendering (SSR) needs a specific user request. Both happen on the server. The difference between static and dynamic rendering is all about the presence or absence of a request.
Requests live in the server context
The HTTP request tells you interesting things like who the user is, via the cookies, or what language they use, via the headers. This information makes up a “server context” for the request. You can use this context to render content that is specific to the current user.
Whether you are a PHP developer maintaining a 20-year-old website or a Next.js developer trying out the latest bleeding-edge features, you have already used the server context more often than you can imagine.
In older versions of Next.js, this notion of context was very obvious. Below is a dummy example of how you could render user-specific content with getServerSideProps
. It should be transparent even to non-Next.js users.
// getServerSideProps fetches data server-side
// it is run on every request to produce a server rendering of the page
export async function getServerSideProps(context) {
// Will print: req, res, params, locale...
// The server context is packed with useful information!
console.log(Object.keys(context))
// The request cookies tell us who sent the request
const user = await getUserFromRequest(context.req)
// The "user" prop is passed to the React component that renders the page
return {
props: { user },
}
}
This context was created directly by Next.js, and used only in this specific method. However, Next.js 13 had to bring major changes in order to support React Server Components (RSC) efficiently.
What about Next.js 13?
Next.js 13, released in October 2022, brought long-awaited features such as layouts, and the support of React Server Components.
There are 2 main differences between an RSC and a “normal” React component:
- The Server Component is not hydrated client-side. This is not relevant to this article, but just so you know, they help improve performance a lot for non-interactive content such as blog posts. It’s basically like pure rendering HTML.
- The Server Component can fetch data server-side. In the past, only top-level pages could do this in Next.js. Now, components that are nested within a page can also communicate directly with a database. This part is very relevant to this article.
Sadly, the “context” object also disappeared in the process, we are not supposed to use the getServerSideProps
function anymore.
Yet it doesn’t mean that there is no server context. On the contrary, it’s even more important! But this context is now implicit. This makes Next.js 13 easier to use, but also slightly harder to understand.
Advanced use cases such as using GraphQL during server-side rendering are harder to implement. For example, to achieve good performance, you need to instantiate a new GraphQL cache for each user request, and this cache should be stored in the server context. Previously, you would do so in getServerSideProps
. But now, you also need to pass the client down to React Server Components, and at the time of writing, there is no obvious way to do so.
We will learn how to solve this problem, but before, we need to understand better the internals of Next.js server rendering.
Checkpoint: what we have learned so far
1. Server-side rendering takes an HTTP request as input and outputs a page.
2. Each request is associated with a server context, that contains useful information: the current request, some derived data (like the current user), cached data, and so on.
3. It was obvious in Next.js 12: you literally had a context
input in the getServerSideProps
method of SSRed pages.
4. In Next.js 13, things get harder: the server context is now hidden, yet it is ubiquitous within React Server Components. It’s all the more important to understand what happens.
Build the most performant websites
Join other developers on the leading edge of development and subscribe to get monthly insights on creating websites that are performant and deliver maximum business value.
How Next.js server-side rendering works
Next.js server-side rendering is data-centric. It means that the data requirements for a page or a component will dictate whether it is static or dynamic, and that data fetching is highly coupled with rendering.
For instance, a component can get the current user's first name from the database and render <h1>Hello Eric!</h1>
; this HTML is then served to the user.
Here is a simplified view of the structure of a Next.js page and its data requirements. You will find more schemas in the official Next.js documentation.
- Layout, Page, Component, and Leaf are all React components, and they are all rendered on the server.
- Leaves are specifically client components: they will be rendered once on the server, and a second time in the browser during hydration. This means they are interactive. However, they can’t call the database directly, since they must be browser-friendly. They can use normal client-side fetch calls.
- Layout, Page, and Component are React Server Components (RSC): they will be rendered once on the server and that’s it. The cool thing is that they can access server-only resources, and do things such as calling the database directly. They can also use server-side fetch calls, that are automatically enhanced by Next.js to support request deduplication when the same request must be sent by multiple components.
This schema is for data fetching. But this schema is incomplete because it doesn’t show the input and output of the rendering process: the request and the response. And of course, it also lacks the context.
Let’s give it another shot:
Server-side rendering is about traversing a tree, from the root layout to leaf components. The context is what you collect on each node. It consists of the initial request, but it also includes all the data your server components are going to fetch. That’s how Next.js implements automatic request deduplication, via an enhanced “fetch” method that uses the server context as a cache. But where does it put the cached data?
A difference between client component and RSCs
Contrary to client components, RSCs cannot call any of the React hooks used to persist state or context. Only client components can call useState
and useContext
. So, there has to be a similar alternative for server components.
Before we answer this question, here’s a pro insight: the tree structure shown earlier is highly similar to how GraphQL servers handle requests. The GraphQL tree is made of “resolvers” instead of React components, but it works roughly the same: you have a request, you compute a response, and you store intermediate useful information in a shared context.
This proves that server-side rendering a page is not that different from answering an API call: it’s all about processing requests the right way. And before you ask, yes, you can use GraphQL to implement backend-driven user interfaces. But should you??
How to read context
We need to read the request context whenever a page depends on the content of the user request. A typical use case is getting some information from the request headers and cookies, like the current user’s language and theme preference.
headers
and cookies
Next.js 13 provides two methods to access the current HTTP request, headers
and cookies
. Here is a pretty straight-forward example of usage, inspired by the official documentation:
import { headers, cookies } from 'next/headers'
export default async function Page() {
// we can access headers
// to tell us where the user comes from
const headersList = headers()
const referer = headersList.get('referer')
// we can get the current user
// based on the authentication token stored in an HTTP-only cookie
const nextCookies = cookies()
const user = await getUserFromToken(cookies.get('auth_token'))
return (
<div>
Referer: {referer}. User: {user.name}
</div>
)
}
These functions are meant to be called in any React Server Component, either a layout, a page, or a nested component.
Passing the server context down to the client
In the browser, the rendering is not related to an HTTP request. A component can be rendered at any time, for instance when the user interacts with the interface, without triggering a new request. If there is no request, there are no headers or cookies either; that’s why you can’t use these methods in client components.
What if a client component still needs to know about the content of the request? Just use props or client context as usual!
Here is how I would solve this problem:
// /app/referer/page.tsx (Server Component)
import { headers } from 'next/headers'
export default async function Page() {
const headersList = headers()
const referer = headersList.get('referer')
return (
// we set a CLIENT context,
// based on the SERVER context that we got via headers()
<RefererProvider value={referer}>
<InteractiveReferer />
</RefererProvider>
)
}
// /components/InteractiveReferer.tsx (Client Component)
// use client
export const InteractiveReferer = () => {
// this context has been initialized by a parent RSC
// but we can access it as expected in a Client Component
const referer = useRefererContext()
const [clickedReferer, setClickedReferer] = useState(false)
return (
<button
onCLick={() => {
setClickedReferer(true)
}}
>
Referer: {referer}
</button>
)
}
React Server Components are able to render Client Components, including context providers, as long as the Client Component lives in a different file. The only limitation is that you can only pass serializable values, excluding functions or class instances.
Cookies and headers are cool. But the request is more than that. What if you want to read the request URL? At the time of writing, with Next.js 13.1, there is no explicit simple solution (read: that does not involve middleware) to achieve something as simple as reading the raw URL from the request. We need to dig deeper.
Checkpoint: what we have learned so far
1. In React, your components form a tree structure, from root to leaves.
2. In Next.js 12, you could access the server context only at the root of this tree of React components, in pages.
3. In Next.js 13, you have React Server Components at multiple levels: layout, page, or nested more deeply. All of them can access the server context, at any level!
4. The cookies
and headers
methods from Next.js 13 are new ways to access the server context cleanly, from any React Server Component, nested or not.
5. You can pass values down to the client-side components too. Be careful not to confuse server context and normal React client-side context.
6. For many use cases, you will need not just to read the context but also to extend it. Let’s describe these use cases more precisely.
You definitely need to extend the server context, and here is how
In GraphQL, it’s pretty common to initialize the server context with derived state. For instance, you can get the current user based on the auth_token
cookie, and put the user data in the context. Resolvers further down the graph can access the current user data directly without fetching them again.
In Next.js, server context extension is indirect and relies on a caching mechanism. Say we want to achieve the same thing as in GraphQL: fetching the current user only once, but using the value in multiple React Server Components.
Using fetch
and cache
to add request results to the context
If we get the current user from an API call, good news: Next.js extends fetch
automatically and caches results per URL.
It works similarly to the browser HTTP cache, both for public use cases (get the same data for all users) or private use cases (for a specific request, get the data only once even if multiple components fetch them). Except you can use it server side.
export default Layout = async () => {
// Fetch the current user the first time
// Next.js automatically enhances "fetch" server-side to support caching
const user = await fetch("/api/user")
console.log("Got user in layout!", user)
//...
}
export default Page = async () => {
// Should get the result from the cache automatically, thanks Next :)
const user = await fetch("/api/user")
// ...
}
export const SomeServerComponent = async () => {
// Should get the result from the cache too!
const user = await fetch("/api/user")
// ...
}
This deduplication and caching logic can be considered as part of the server context. The context is invisible to you, the developer, yet the current user is stored somewhere, in a cache, for the duration of the current server rendering.
As a default, cached data are tied to the current request only: each user will see their own data and not the profile of their neighbor!
For public data, you can use the force-cache
option, so that the result is reused for all requests and not just the current one.
However, not all APIs use HTTP. React Server Components allow you to run any kind of server code, so why not call our database directly? In order to benefit from the same server-side deduplication feature that Next.js fetch
provides automatically, you can use React 18’s cache
function.
The official example from Next.js documentation speaks for itself:
import { cache } from 'react';
// thanks to the "cache" function,
// calls will be deduplicated
export const getUser = cache(async (id) => {
const user = await db.user.findUnique({ id });
return user;
});
// you can reuse "getUser" in as many component as you want
// the database will be queried only once per HTTP request to a page
Granular caching with server context enhancement
Next.js’ caching mechanism lets you enhance the server context implicitly. It is suitable when the exact same request is fired twice or more, and it’s a kind of server context enhancement.
It falls short when facing more advanced use cases. Say you have 2 functions: getManyAuthors
and getOneAuthor
.
Since they are different requests, if getManyAuthors()
gets author A and author B, then a call to getOneAuthor(A)
will still need to fire a new request! It won’t be able to detect that we already fetched a list of authors containing author “A.”
If you have used GraphQL or a modern client-side fetching library with advanced caching features, this will sound disappointing. Ideally, authors should be cached based on their unique id
, not based on the request used to fetch them.
Hey, why not use such a library server-side to resolve this issue? The sky is the limit after all!
GraphQL clients are most often SSR-friendly. You can certainly use Apollo Client server-side, during the server render, and enjoy its caching capabilities.
To achieve this, you need to instantiate a new client per request, and then put it in … yes you got it, in the server context! This way the same client can be reused by the layout, the page, and the nested server components.
Here is what a server render of a page using GraphQL looks like. The same instance of ApolloClient is used by all components, so they enjoy Apollo caching features.
A schema is great, but what about the corresponding code? Well, we’ve met our first issue from earlier: we cannot access the raw HTTP request easily in a React Server Component. Here is a second issue: it seems hard to extend the server context with new values, such as an instance of a data fetching library carrying its own advanced caching logic. More precisely, there is no documented way to achieve this as of Next.js version 13.1. But don’t worry; we will discover a simple solution in the next section.
You probably didn’t start to read this article just to discover a bunch of issues, though! Let’s move forward and try to figure out a solution!
Checkpoint: what we have learned so far
1. In your app, multiple React Server Components can share the same data requirements. This could lead to duplicating the same requests and create performance issues during SSR.
2. Next.js 13 provides two built-in ways to fix this issue: fetch is automatically patched server-side to cache HTTP requests for you; cache works the same but it’s more generic. You can use it to call a database for instance.
3. This approach works well if two requests are exactly the same. But it doesn’t work if two requests are just slightly different, like fetching a list of authors and then a single author that was already in the list.
4. A possible solution: instead of caching data, we can cache… a cache. Yeah, you’ve read it correctly! Let’s learn how to achieve that.
Extend the store with cache
I’ve encountered a few early experiments in the wild, with Apollo and with tRPC, that hack Next.js context directly to extend it with more values🕵️. Next.js handles the server context internally via Node.js AsyncLocalStorage
, and it’s quite complex. Probably worth an article of its own (stay tuned!).
Hopefully, we don’t have to go this far to extend the server context with our own values. The best solution is much, much simpler✌️!
Do you remember the cache
function from a few paragraphs above? It turns out that this function is not limited to data fetching; you can also cache functions that return any kind of value, for instance, a GraphQL client!
// this client will be cached for the duration of the request
// => it's a perfect way to craft some server context!
const getApolloClient = cache(() => createApolloClient())
That’s it! The point is that the cache
is bound to the current request. Instead of thinking about the server context as an object, you can think about it as a set of functions that return cached objects.
You can think of this pattern as “caching the cache,” instead of the data. Keep in mind that it’s an optimization pattern. If you don’t have an obvious use case for this, like calling a GraphQL API, don’t worry: you probably don’t need this approach.
This behavior is barely documented at the moment, and the implementation of cache
lives in the React code base, not Next’s. You can get more details from this Twitter thread by Sebastian Markbåge and check this example from Andy Ingram using DataLoader. I’ve also crafted a generic open source demonstration of server context just for you!
Final review
It is important to understand the role of the “server context” to implement advanced features, namely to efficiently cache complex requests.
This server-side context is symmetrical to the React client-side context:
- Client-side: when you refresh the page, React will generate a new client-side context. The context can then be read or modified by components during the client render. The context lives as long as you don’t close or refresh the page.
- Server-side: when you refresh a page, Next.js will generate a new server-side context. The context can then be read or modified by React Server Components during the server render. This context is tied to the request; it is recreated for each new request and disappears after you got your response with the initial server render.
We have described the relation between server context and Next.js/React 18 headers
, cookies
, enhanced fetch
, and cache
methods. Server context is a concept you’ll encounter in many other situations, so learning how we can read but also extend it should be useful to web developers.
Given the importance of the use cases we have listed, such as supporting efficient server-side rendering of GraphQL requests, I am pretty confident that Next.js’ core team and community will produce more resources about server context in coming days. And I hope that thanks to this article, you’ll be ready to get the best value out of them!
Keep learning
More resources
- If you plan to use Redux in server components, you will most probably need to put your store in the server context. This ticket tracks related Redux documentation improvements.
- “Backend For Frontend” is a paradigm where API endpoints for the frontend are structured per page (”/api/home”, “/api/account/profile”). You can use an API route for that, but you could also use a React Server Component. If you use an RSC, you will probably need to access the server context because the response depends on the request. Here’s where you can learn more about this.
- Python recently introduced
contextvars
, which are quite similar to Node.js’
AsyncLocalStorage.
- Next Connect and Next Runtime experimented with surprising ways to hack Next 12 server context, namely the HTTP response object, to make each page behave like an API, similar to Remix actions. Check them out here and here.
- Check out Next Server Context, an experiment to carry around the HTTP request context in React client components (the architecture of the code with ESM and without bundler is also interesting in itself, Jayden Seric has explored this area a lot).
- React fetching data with
useEffect
, improved documentation. It shows that library makers might need to use more advanced features, like in ReactuseEffect
for data fetching should usually be wrapped in a cleaner library like “SWR”, “react-query”, etc. But exploring the internals is still cool! - Here’s the RFC that defines async data fetching in React. It doesn’t cover caching yet :( Though cache can at least already be used in Server Components as demonstrated in the Next.js documentation.
- HTTP Cache is used by fetch in the browser. Next.js enables the same feature server-side, via the server context.