Dennis Persson
Dennis Persson

Reputation: 1104

How to use cookies in Client Components in Next.js 13 and read them on initial SSR request when dynamic routing is enabled?

I really can't figure out what Next.js have for plan to make cookies work in client components. The current docs they have for cookies is not compatible with client components.

In server components, you can read and set the cookies using server actions. With client components, you can set them with server actions, but it doesn't look like you can read the cookies. Trying to use cookies().get(name) results in the error below.

You're importing a component that needs next/headers. That only works in a Server

...

Component but one of its parents is marked with "use client", so it's a Client Component. One of these is marked as a client entry with "use client":
./app/client/hooks/useCookies.ts
./app/client/components/SomeComponent.tsx

Of course, you can add a server actions to read the cookie, that I have tested to see that it works. But I refuse to mess up my code with async function handling and unnecessary server trips just to read a cookie that already lies right there in the browser.

I have also tried libraries, like cookies-next, which seems to be best maintained and working lib for Next.js cookies. That one works to read and set cookies in client components on client side.

The issue using that library is that the initial rendering flickers, since that library doesn't seem to be able to read the cookies server side with app routing. Even if I manually enable dynamic routing using Route Segment Config all cookies are undefined on the initial SSR request. It's first when I reach the client the cookies are found and read correctly. And with static rendering, cookies obviously cannot be read.

I have also tried setting the cookies using Next.js built in cookie system and then reading them with cookies-next library in my client components. Even using that solution, cookies are undefined on the initial SSR request.

Therefore I wonder, can anyone tell me if there is a way to make these two conditions work together when using Next.js app routing?

Do I really have to read cookies in a server component and pass the cookie values down to client components as serialised values? Which also means that they will not be automatically updated when I set new values for the cookie.

I feel there is something working very wrong here, or I hope I have totally missed out something. Whole purpose with cookies is that you should be able to read them both client and server side.

Upvotes: 28

Views: 37781

Answers (5)

Mateusz Bałdyga
Mateusz Bałdyga

Reputation: 1

You can do this:

import JSCookie from 'js-cookie';

export function getCookie(name: string) {
  if (typeof window === 'undefined') {
    // Read a cookie server-side
    return require('next/headers').cookies().get(name)?.value;
  }

  // Read a cookie client-side
  return JSCookie.get(name);
}

Upvotes: 0

netrolite
netrolite

Reputation: 2813

Apparently the only solution to that is to convert your client component to a server component.

Sometimes that doesn't seem possible because you need to pass state from the top of the component tree, but I've come up with a solution. For example, let's say you have a component named Parent and a component named Child:

Parent.tsx:

"use client";

export default function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Child setCount={setCount} count={count} />
    </div>
  );
}

Child.tsx:

export default function Child({
  count,
  setCount,
}: {
  count: number;
  setCount: Dispatch<SetStateAction<number>>;
}) {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
    </div>
  );
}

As you can see, we define 2 state variables: count and setCount and pass them down to the Child component. Now, let's say we want to read a cookie from the parent component. The only way to to that is to convert the Parent into a server component. How do we do that? Well, we can create a provider component that wraps the Parent and passes down the necessary state using a react context. I'm going to name the provider component PageContextProvider. This is how we should update our code:

PageContextProvider.tsx:

// the provider has to be a client component, but this doesn't affect any of its children
"use client";
import { createContext, Dispatch, ReactNode, SetStateAction, useState } from "react";

interface TPageContext {
  count: number;
  setCount: Dispatch<SetStateAction<number>>;
}

export const PageContext = createContext<TPageContext | null>(null);

export default function PageContextProvider({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <PageContext.Provider value={{ count, setCount }}>
      {children}
    </PageContext.Provider>
  );
}

Parent.tsx:

// no more "use client"!
import { cookies } from "next/headers";
import Child from "./Child";
import PageContextProvider from "./provider";

export default function Parent() {
  // we can now safely read cookies!
  const cookiesStore = cookies();
  const someCookie = cookiesStore.get("someCookie")?.value;

  return (
    <PageContextProvider>
      <div>
        <Child />
      </div>
    </PageContextProvider>
  );
}

Child.tsx:

"use client"; // `Parent` is no longer a client component, so add this line!
import { useContext } from "react";
import { PageContext } from "./provider";

export default function Child() {
  // now we get `count` and `setCount` from the context, so there's no need to pass any props into `Child`
  const pageContext = useContext(PageContext);

  // making sure context isn't `null`, which is its initial value
  if (!pageContext) throw new Error("context is null");

  const { count, setCount } = pageContext; // destructuring the needed values

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
    </div>
  );
}

Of course in this example you could just move all the client logic into the ‘Child’ component but I was trying to illustrate what you could do if, for instance, you needed to share some state between the “leaves” of your component tree.

Upvotes: 0

teos
teos

Reputation: 427

One common confusion about Client Components is that they are also rendered on the server (once, on initialization), and sent back as pure HTML/CSS.
They are hydrated in the browser later to add javascript.

See Next.js - What is Streaming?

At that point, javascript libraries like cookies-next or js-cookie won't run as you would expect since they are not actually running in your browser (there is no window or document, we are server-side).

That means that the very first render may be incomplete in a Client Component, and it won't load the data you expect it to.
That's ok in most cases because it only last for a short instant. The idea is simply to deliver the best static placeholder/shell possible, but in your case that's what is causing the flickering effect

You can observe it yourself by disabling javascrit in your browser. You will see all your Client Components load, but probably not in the state you expected.

In short:

  • A Client Component, have access to document cookies via cookies-next (not via next/headers), but the first (static) render won't get any data from it.
  • A Server Component, on the other hand, have access to HTTP cookies via next/headers (but not via cookies-next).

You cannot have both behavior in the same component
I believe that's by design, there is no way for a function to work in both spaces.


But, you can build a Client Component and wrap it into Server Component, like so:

page.tsx

import { cookies } from 'next/headers'
import { MyClientComponent } from './MyClientComponent'

export default function Page() {
    // Server-side: based on HTTP resquest cookie only
    const bgColor = cookies().get('bgColor')?.value
    return <MyClientComponent initial={{ bgColor }} />
}

MyClientComponent.tsx

'use client'

import { getCookie, setCookie } from 'cookies-next'
import { useState, useEffect } from 'react'

export function MyClientComponent({
    initial,
}: {
    initial: { bgColor?: string }
}) {
    const [bgColor, setBgColor] = useState(
        getCookie('bgColor')?.toString() ?? initial.bgColor ?? '',
    )

    useEffect(() => {
        setCookie('bgColor', bgColor)
    }, [bgColor])

    return (
        <input
            style={{ backgroundColor: bgColor }}
            value={bgColor}
            onChange={({ target }) => {
                setBgColor(target.value)
            }}
        />
    )
}

Also:

  • Always use Link, so that the page is not refreshed while navigating, limiting the flickering effect to the very first (server-side) rendering only

Upvotes: 18

Debarka Mondal
Debarka Mondal

Reputation: 53

Please mention what you have tried. I don't know if I understood the problem properly but I hope this helps.

import { cookies } from 'next/headers'
 
export default function Page() {
  const cookieStore = cookies()
  const theme = cookieStore.get('theme')
  return '...'
}

Please check out Next Documentatoin for more information.

Upvotes: 1

Tony
Tony

Reputation: 89

I have also tried and gained experience: client cookies are available in client components, and server cookies are available in server components. These different cookies are not accessible to each other. It looks like this is another flaw in NextJS

Upvotes: 8

Related Questions