โ€” ยท 7 min read Min read

Next 13 cookies() and headers() explained

So, Next.js 13 has some exciting new features in store for us - the cookies() and headers() functions for server components. But what exactly are they? Why only available in server components? Let's dive in and demystify these functions!

A piece of code importing and using the cookies function from next/headers

In a nutshell, these functions allow us to access cookies and headers from anywhere within the React tree, as long as we're dealing with a server component. They're like globals, but not the typical globals you can use anywhere in your code. These babies are exclusive to React server components.

Let's see an example of how they work:

import { headers } from 'next/headers';
 
export default function MyComponent() {
  const host = headers().get('host');
  return <div>{host}</div>; // will print localhost:3000
}

In this case, we're using the headers() function to retrieve the 'host' header and display it in our component. Nice, right?

Peeking into the source code

Alright, so how do these functions really work under the hood? I did some snooping around the Next.js repository and found them in the file packages/next/src/client/components/headers.ts. Here's a snippet of the headers() function:

export function headers() {
  // that's for returning empty headers when doing static generation
  if (staticGenerationBailout('headers')) {
    return HeadersAdapter.seal(new Headers({}));
  }
 
  // wat is dis ??
  const requestStore = requestAsyncStorage.getStore(); // ๐Ÿง๐Ÿง๐Ÿง
  if (!requestStore) {
    throw new Error(
      `Invariant: Method expects to have requestAsyncStorage, none available`,
    );
  }
 
  return requestStore.headers;
}

What is this "store" I've never heard of?

Digging a bit further led me to the file packages/next/src/client/components/async-local-storage.ts. The file starts with:

import type { AsyncLocalStorage } from 'async_hooks';
...๐Ÿคฏ

Turns out node.js has a module called async_hooks, containing an AsyncLocalStorage thing that allows you to make data available from anywhere inside code that runs "beneath" it. It's like a React Context, but in node.js. You can also think of it like a css variable: you define it at some level, and it's available to all its children.

A comparison side by side with css variables

And it has been there since node.js 14. Looks like I'm late to the party. ๐Ÿ˜…

Should we try to build our own http server to understand how it works?

Of course we should!

Building our own server components (who said baitclick?)

Let's start with a simple react implementation made by Tejas Kumar in this video: Write React Server Components from Scratch . (Watch it, suscribe to his channel and click the bell icon ๐Ÿซต)

Start by cloning the repo and installing dependencies:

git clone https://github.com/raphaelbadia/rsc-from-scratch-with-headers.git
pnpm install

Then compile the server:

pnpm run build

And start the server:

node dist/server.js

Once the server is up and running, head to http://localhost:3000/list to see it in action.

A screenshot of the website running

I'll let you dig into the code of server.tsx: it's a simple http server that renders a react component.

const app = express();
app.use(express.static("./dist"));
app.get("/:page", async (req, res) => {
  // calls the server components inside the pages folder
  ...
});

Rolling our own cookies() and headers()

Let's create a new file alongside server.tsx called requestStorage.ts. It will contain our mysterious AsyncStorage.

// requestStorage.ts
const { AsyncLocalStorage } = require('async_hooks');
 
export const requestStorage = new AsyncLocalStorage();

requestStorage now holds a run() function that takes two arguments : the store to be shared with its children, and a callback function that should wrap the code that needs access to the store.

Then we need to import it in server.tsx. In nextjs, the cookies() and headers() functions are per-request cached. So we need to create a new instance of our storage for each request.

 import { join } from "path";
 
 import Layout from "./layout";
+import { requestStorage } from "./requestStorage";
 
 const app = express();
 app.use(express.static("./dist"));
 
 app.get("/:page", async (req, res) => {
+  requestStorage.run("hello world!", async () => {
     try {
       res.end(html);
       ......
     } catch (e) {
       res.end("not supported");
     }
+  });
 });

You can now open one of the react server components, for instance pages/list.tsx, and import our requestStorage. What if we try to log the store?

 import { requestStorage } from "../requestStorage";
 
 export default function List() {
  console.log(requestStorage.getStore());
  ...
 }

If you refresh the page, you should see the following output in the terminal:

$ node dist/server.js
Listening on 3000!
hello guys!

We just read the string sent from server.tsx in a server component! ๐ŸŽ‰

From now on, you can guess easily how to make your own cookies() and headers() functions. You just need to add the headers and cookies properties to the store, parsed from express's req object.

Create a file called headers.ts in the same folder as requestStorage.ts:

// headers.ts
import { requestStorage } from './requestStorage';
 
export function cookies() {
  return requestStorage.getStore().cookies;
}
 
export function headers() {
  return requestStorage.getStore().headers;
}

Now, update server.tsx to add the headers and cookies to the store:

// server.tsx
app.get("/:page", async (req, res) => {
+  const storeHeaders = new Map();
+  for (const [key, value] of Object.entries(req.headers)) {
+    storeHeaders.set(key, value);
+  }
+  const storeCookies = new URLSearchParams(
+    req.headers.cookie.split("; ").join("&") // of course, don't do this IRL
+  );
   requestStorage.run(
+    { headers: storeHeaders, cookies: storeCookies },
  // nothing changes after that
  ...
  )
});

And finally, update list.tsx to use the new functions:

// list.tsx
- import { requestStorage } from "../requestStorage";
+ import { cookies, headers } from "../headers";
 
  export default function List() {
-   console.log(requestStorage.getStore());
+   console.log(headers().get('host'));
   ...
  }

Refresh the page, and you should see your host in the terminal! ๐ŸŽ‰

Conclusion : a real-world use case

Now that you've understood how cookies() and headers() function work, can you think of a use case where this knowledge could be useful?

I do! I'm currently working on a project where I need to access the headers in a middleware. I disliked having to pass the request object everywhere down the function stream so I've used this technique to make the headers available everywhere in middlewares!

You can learn about it here on npm โžก๏ธ next-headers: A (very) tiny wrapper to allow you to use Next.js's cookies() and headers() functions in your middleware.

๐Ÿ‘‹