Introduction
Next.js v13
Next.js 13, the highly popular React framework, introduces a raft of transformative features aimed at creating more dynamic and efficient web experiences. Leveraging the utility of the new app directory, the system enables easier and faster routing and layouts. The entire process becomes streamlined with layouts allowing shared UI between routes, state preservation, and a reduction in re-renders. Next.js 13 also supports server components for the most dynamic applications and introduces streaming to allow for instant loading states.
A significant development is the introduction of Turbopack, a Rust-based Webpack replacement, promising up to 700x faster updates. It comes with a new and improved next/image
component that supports faster native browser lazy loading. The @next/font
component makes automatic self-hosting of fonts easier, eliminating layout shifts. Reinventing the next/link
API simplifies making automatic <a>
. The version also includes a host of incremental improvements to Next.js 12 that collectively promise a better, faster, and more developer-friendly experience.
Client and server-side components
One of the key updates in Next.js 13 is the support for both Client Components and Server Components, which are part of React's new Server Components architecture. This setup allows developers to leverage both the server and client to optimally use what each is great at, facilitating the creation of highly interactive applications with a single programming model.
Server Components are designed to render on the server and send HTML directly to the client. These components enable developers to build complex user interfaces while significantly reducing the amount of JavaScript sent to the client as they do not contain client-side interactivity. This makes it faster for initial page loads and enables a server-first approach with a controlled runtime size that remains constant, irrespective of your application's growth.
On the other hand, Client Components run either on the client-side or the server-side, and can use the full range of React's capabilities, including state, effects, and access to the DOM. Client components are hydration boundary, which means other components outside this boundary won't need to be hydrated.
With this flexibility, developers can decide which components of their app need the power and capability of the client, and which can be pre-rendered on the server side, enhancing performance and user experience. This also improves performance due to efficient code-splitting, as only required components are loaded for a given route, reducing the amount of JavaScript that needs to be parsed and executed.
The problem with cookies
While Next.js 13 is a significant improvement over its predecessor, it does have its share of issues. One of the most significant issues is the lack of cookies during server-side rendering (SSR). This is a problem that has been around for a while, and with no official solution in sight, developers have had to come up with their own workarounds.
Next.js does not have access to cookies during the server-side rendering (SSR), while the client does. This means that if you set a cookie on the client-side, it will not be available during server-side rendering and can break your hydrated application.
It's worth mention that server-side components also can't change cookies, as it will change the application state and require sending headers to the client before the component is rendered. So we have a weird situation that reading cookies using the cookies()
helper is possible, but changing them is not.
Searching for a solution
Context provider
In orderr to solve this problem I had no choise but to create some sort of a wrapper to Next.js server cookies()
and allow to use it while rendering client-side components on the server. The best way to do it is to create a custom context provider that will read the cookies from the server and pass them to the client-side components:
import { cookies } from 'next/headers'; import { ClientCookiesProvider } from './provider'; export default function RootLayout({ children }) { return ( <ClientCookiesProvider value={cookies().getAll()}> {children} </ClientCookiesProvider> ); }
THe provider itself is a simple React context provider that passes the cookies to the useCookies()
hook:
'use client'; import { createContext, useContext } from 'react'; const CookiesContext = createContext(null); export const ClientCookiesProvider = ({ value, children }) => { return ( <CookiesContext.Provider value={value}>{children}</CookiesContext.Provider> ); };
useCookies()
hook
Great! Now let's make sure our useCookies()
hook can create and update cookies as well as read them.
We'll use the js-cookie
package for the client side:
export const useCookies = (): Cookies => { if (typeof window !== 'undefined') { return jsCookies; } // ... };
For the server-side rendering we will need to use a little hack from next/navigation
called ServerInsertedHTMLContext
:
import { ServerInsertedHTMLContext } from 'next/navigation'; // usage example const insertedHTML = useContext(ServerInsertedHTMLContext); insertedHTML?.(() => ( <script dangerouslySetInnerHTML={{ __html: `alert("Hello world!");`, }} /> ));
This will allow us to inject JavaScript code to the client and execute the changes we need when the client is rendered. In addition, I've created a full useCookies()
implemtation that will match the js-cookie
API (you can see the full source code here).
Conclusion
The next-client-cookies
package is a simple solution to a complex problem. It allows you to use cookies in your Next.js application without having to worry about SSR issues. It's easy to use and works with Next.js v13+, so you can start using it today!
👉 Check out the package on GitHub and let me know what you think.
If you have any questions or feedback, please leave them in the comments below. I'd love to hear from you!