⚡ Unlock faster workflows with Prismic’s newest features!See what's new and upcoming
Performance & UX
·12 min read

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.

A diagram visually representing the React components tree described below. It shows server-side components calling the database and an API, while client components only call the API.
  • 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:

The same React components tree as the last diagram, but this time, the process of a request progressing to a response on the client side is shown, along with a line representing context that travels across all server-side components before joining the response.

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.

The same diagram as the last one, but now we see that a new ApolloClient() creates the context at the top of the tree diagram.

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!

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 React useEffect 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.
Article written by

Eric Burel

A French developer who's been creating startup SaaS products for over five years, shares his Next.js knowledge as a teacher for Human Coders, and generally loves diving into code to explore complex challenges.

More posts
Eric Burel

7 comments

Joel

But how can you use this cache mechanism when deploying your Next application in AWS ECS since they don’t have shared context and each request will not get the response from the same server?
Reply·1 year ago

Eric

This is a reply to Joel's comment

But how can you use this cache mechanism when deploying your Next application in AWS ECS since they don’t have shared context and each request will not get the response from the same server?

Hi Joel! React 18 "cache" function is scoped to a single request/response round. So, different requests will never ever share the same cache value. For instance, you are not supposed to put a database connection in this cache. Instead, you should make it a global variable that will be shared for all users hitting the same server. In this cache, you can, however, put a function that gets the logged-in user for the current request, because multiple React Server Components may use this value down the rendering tree. Exactly like how you use React Context client-side, it's local to your machine but not shared with other people. I hope this helps!
Reply·1 year ago

Adam

I've been trying to build an SDK that uses this approach, but the problem I've run into is "when does the cache setting run"? For example, I want developers to be able to instantiate an instance of my SDK's client in one location in their serverside code, and have it be available across all the server components further down in the tree. Execute the initialization in the root layout? That doesn't work, since the root layout isn't re-rendered on every request. Middleware? That's run in a different instance and doesn't seem to share the cache On every page? That works but it's really annoying ergonomics because you have to pass the options in for init in every location. I opened a discussion about it here. https://github.com/vercel/next.js/discussions/60124
Reply·10 months ago

Angelo

This is a reply to Adam's comment

I've been trying to build an SDK that uses this approach, but the problem I've run into is "when does the cache setting run"? For example, I want developers to be able to instantiate an instance of my SDK's client in one location in their serverside code, and have it be available across all the server components further down in the tree. Execute the initialization in the root layout? That doesn't work, since the root layout isn't re-rendered on every request. Middleware? That's run in a different instance and doesn't seem to share the cache On every page? That works but it's really annoying ergonomics because you have to pass the options in for init in every location. I opened a discussion about it here. https://github.com/vercel/next.js/discussions/60124

Sharing context among server components doesn't map directly to practices we use in client components. Server components are efficient, especially when paired with Next.js' cache.

How you work with data throughout your app depends on your use case:

- If your data comes from a networked API, use fetch() to call the API wherever you need the data rather than sharing it through a central store. Next.js' fetch() cache is smart enough to deduplicate requests so you are not wasting resources.

- If you data comes from a local source, such as the file system, use cache() wherever you need the data rather than sharing it through a central store. It is best to architect your app such that rendering order does not matter. In the case of cache(), it is smart enough to perform an action once in the tree (where is likely non-deterministic) and recall it elsewhere.

- If your data comes from the client, such as user input through a form, you will need to use client components and a client-side global state system, like context. You will need to carefully craft the component tree if you need to revert back to server components for security or performance purposes.

Some other considerations:

- If the data can change on each request, you will need to use dynamic rendering if you are not already.

- Specifically for feature flags, is it possible to use something lower-level like reading environment variables directly? For example, reading process.env.ENABLE_TRACKING in your components.

It's possible there are use cases that are not covered by the above recommendations, but starting simple and removing abstractions where possible is ideal.

Reply·10 months ago

barroudjo

Just to let you know, your post was the starting point for my library rsc-better-cache which simplifies sharing data sharing across server components (especially data obtained asynchronously).

Thanks !

Reply·10 months ago

Eric Burel

This is a reply to barroudjo's comment

Just to let you know, your post was the starting point for my library rsc-better-cache which simplifies sharing data sharing across server components (especially data obtained asynchronously).

Thanks !

Hey that's super nice glad it helped! It's been used in this library too to implement search params parsing and sharing in the RSCs tree: https://github.com/47ng/nuqs

Reply·10 months ago

Eric BUrel

This is a reply to Adam's comment

I've been trying to build an SDK that uses this approach, but the problem I've run into is "when does the cache setting run"? For example, I want developers to be able to instantiate an instance of my SDK's client in one location in their serverside code, and have it be available across all the server components further down in the tree. Execute the initialization in the root layout? That doesn't work, since the root layout isn't re-rendered on every request. Middleware? That's run in a different instance and doesn't seem to share the cache On every page? That works but it's really annoying ergonomics because you have to pass the options in for init in every location. I opened a discussion about it here. https://github.com/vercel/next.js/discussions/60124

Hi, the point of cache is that the place where you initialize doesn't matter, as long as it is cached. The typical example is an Apollo GraphQL client. The first component that gets rendered will fire the initialization, and the other will get the cached value.

Initializing things in a very specific component is not really idiomatic of Next.js and serverless hosting, so I am not totally sure what's your goal here. Perhaps a custom server would be more suited in your case?

I feel like you might need cache across multiple requests and not just the current one? For cross-requests concerns, you can instead use "unstable_cache" from Next (be wary that Next.js unstable_cache and React cache are totally different things despite the similar name) or just a global node-cache.

You may also be interested in getting a finer control over caching via this option: https://nextjs.org/docs/app/api-reference/next-config-js/incrementalCacheHandlerPath

Reply·10 months ago
Hit your website goals

Websites success stories from the Prismic Community

How Arcadia is Telling a Consistent Brand Story

Read Case Study

How Evri Cut their Time to Ship

Read Case Study

How Pallyy Grew Daily Visitors from 500 to 10,000

Read Case Study