Summer 2022
Next.js App Router: Handling Cookies
When working on the rebuild of the dashboard at Covie, I opted to use the app router that was introduced in v13.
To make full use of the SSR / CSR features that come with Next.js, I was making API requests from middleware, server and client.
Due to how cookies are passed between server and client and the current state of Next.js various APIs surrounding this, this ended up being a little more complicated than first anticipated.
Authentication
Previously when implementing authentication into Next.js projects, I would have a auth hook, which in its simplest form, looked something like this:
We can then use this hook and reference isLoggedOut
on individual pages / components to only show content to logged in users.
Alternatively, you can bake the redirect logic into the hook itself with a simple flag:
You'd then call the hook with:
This approach works fine when you're app is largely client side rendered. The main downfall is that you may encounter a flash of UI before logging out or loading spinners while you're validating the user on the initial render. Moving between pages is largely fine, as you'll be making use of caching via <Link />
or SWR itself.
Combining SSR and CSR
When rebuilding the dashboard, I wanted to make use of SSR. There was a range of global information I was fetching on every page, such as the user, selected account and selected application.
If I could fetch and cache this information on the initial render before the client rendered anything, it would reduce the amount of skeleton UI and spinners being shown.
I also wanted to improve on how we were refreshing the set of tiered accessing tokens, as some needed refreshing every 30 minutes.
The overall fetching structure would look like:
- Use middleware to enforce authentication and token renewal
- Use SSR to load initial page information
- Use CSR on dynamic pages (filters, mutations, etc)
Cookies
As we'd be using a blend of server side and client side rendering, we'd need to some way of persisting information, such as access tokens, to make it accessible in both environments.
Local storage is stored within the browser itself, so the server wouldn't have access to this information. Cookies were the next logical step, as these could be transferred via headers.
The main issue with this is that in the Next.js app directory, cookies can only be written in actions, route handlers or client components (via an external package such as js-cookie or in my case next client cookies).
Middleware
Using middleware, you can read and write cookies via the response:
While it seems you can set cookies in middleware, there's an issue where the values are not available on the first render pass within a server component. This is a problem... if we've had to refresh an access token in middleware, it won't be available to pages where we need to fetch information via SSR - resulting in a failed request.
There's currently a work around where you essentially overwrite the headers so that the cookies are set on the initial request and not just the response. This gives you access to cookies you have modified in middleware on the first render within SSR components.
Server side
You can read cookies in SSR components as follows:
To date, you still cannot write cookies in server components. This poses a bit of a problem when dealing with access tokens and the need to refresh them; fairly regularly, in our case.
As I was refreshing tokens in middleware, and middleware will always run before an SSR component, it means that in theory I shouldn't ever have to refresh a token within an SSR component (unless the token has been revoked). This meant I could still safely make requests server side, such as loading initial global and page data.
Client side
Client side is a little easier, as we're interacting with the browser. First, cookies sent with the server response need to be synced client side. I used next client cookies for this.
You can then read / write cookies directly with:
Abstracting
When building an app, I tend to wrap request handling in my own API handler. This will handle things such as:
- Appending relevant headers (such as auth)
- Handling token refresh attempts
- CRUD functions (get, post, delete, put, file uploads, etc)
- Contextual functions (getUser, getAccount, getApplication, etc)
You'll notice previously that in each instance of middleware, server and client, we're accessing cookies via different methods. In order to use my own API layer across all environments, I needed to add another layer on top of the request abstraction to handle this.
This results in various initiators:
Within these instances, I can then wrap the cookie logic and pass reference functions into the API layer.
Let's take the middleware client as an example:
We now have a single local API layer to maintain with different entry points depending on the environment.
Of course you'd want to abstract this further as functionality grows, but this post was mainly to illustrate the ways in which Next.js currently interfaces cookies in the app directory and how I approached it.
I'm sure at some point the team at Next.js will engineer a solution resulting in a single cookies function which detects the environment it's being accessed from and handles the access accordingly. There's no doubt this is causing a bit of headache with the nature in which cookies are stored and transferred between environments.