Reputation: 142
** Setting cookies in server action work around**
I've read the questions and answers from (hlasensky and AnasSafi)[https://stackoverflow.com/questions/76483495/cant-set-cookie-in-server-action-next-js-13-4] but I must say it's not quite clear how or even what they did (lack of code problem and example of solution)
So, here is my problem.
I have this server action, which is supposed to create a cookie and store the user preferred language.
'use server';
import { Locale } from "@/lib/translations/i18n.config";
import { cookies } from 'next/headers';
export default async function setLocaleCookie(locale: Locale) {
cookies().set({
name: 'NEXT_LOCALE',
value: locale,
path: '/',
maxAge: 60 * 60 * 24 * 30, // 30 days
})
console.log("NEXT_LOCALE cookie set to:", locale)
}
Next I need to call this function when the user chooses its preferred lang.
So, I made a component (preferred to be client but can be converted to server if required).
(/components/LocaleSwitcher.tsx)
'use client';
import { i18nConfig, Locale } from '@/lib/translations/i18n.config';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Button } from '../ui/button';
import setLocaleCookie from '@/lib/server_actions/cookies/locale';
export default function LocaleSwitcher() {
const pathName = usePathname()
function redirectedPathName(locale: Locale) {
if (!pathName) return '/'
const segments = pathName.split('/')
segments[1] = locale
return segments.join('/')
}
const locales = i18nConfig.locales
return (
<ul className='flex gap-x-3'>
{locales.map(locale => {
return (
<li key={locale}>
<Link
href={redirectedPathName(locale)}
className='rounded-md border bg-black px-3 py-2 text-white'
locale={locale}
>
{locale}
</Link>
<form action={setLocaleCookie(locale)}>
<Button
type="submit"
className='rounded-md border'
>
{locale}
</Button>
</form>
</li>
)
})}
</ul>
)
}
This component is located inside a Header server component
(/components/Header.tsx):
/** @format */
import { User, getServerSession } from 'next-auth';
import Logo from './Logo';
import NavigationWeb from './NavigationWeb';
import UserMenu from './UserMenu';
import LocaleSwitcher from './LocaleSwitcher';
import { authOptions } from '@/lib/helpers/authOptions';
import { Locale } from '@/lib/translations/i18n.config';
interface NavbarProps {
user?: User;
locale?: Locale;
}
export default async function Header({ user, locale }: NavbarProps) {
const session = await getServerSession(authOptions);
user = session?.user;
return (
<div className="z-10 border-b py-4 px-16 w-full">
<div className="flex flex-row items-center justify-between gap-3 md:gap-0">
<Logo />
<div className="flex justify-between items-center gap-4">
<NavigationWeb />
<UserMenu user={user} />
<LocaleSwitcher /> // HERE IS THE LOCALE SWITCHER COMPONENT
</div>
</div>
</div>
);
}
Which itself is nested in the RootLayout (app/[locale]/layout.ts):
/** @format */
import { Nunito, Inter } from 'next/font/google';
import './globals.css';
import Header from '@/components/header/Header';
import NextAuthSessionProvider from '@/components/providers/SessionProvider';
import { ThemeProvider } from '@/components/providers/ThemeProvider';
import Footer from '@/components/footer/Footer';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/helpers/authOptions';
import localFont from 'next/font/local';
import { Toaster } from '@/components/ui/toaster';
import { i18nConfig, Locale } from '@/lib/translations/i18n.config';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
const nunito = Nunito({
subsets: ['latin'],
display: 'swap',
variable: '--font-nunito',
});
const gambettaReg = localFont({
src: './fonts/Gambetta-Regular.woff2',
display: 'swap',
variable: '--font-gambetta-reg',
});
const gambettaSemi = localFont({
src: './fonts/Gambetta-Semibold.woff2',
display: 'swap',
variable: '--font-gambetta-semi',
});
const circularBold = localFont({
src: './fonts/CircularXXWeb-Bold.woff2',
display: 'swap',
variable: '--font-circular-bold',
});
export const metadata = {
title: 'App Template',
description: 'A template for Next.js applications',
};
export async function generateStaticParams() {
return i18nConfig.locales.map(locale => ({ locale: locale }))
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: Locale };
}) {
const session = await getServerSession(authOptions);
return (
<NextAuthSessionProvider session={session}>
<html lang={params.locale}>
<body
className={`${circularBold.variable} ${gambettaReg.variable} ${gambettaSemi.variable} ${nunito.variable} ${inter.variable} font-inter`}
>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<main className="flex flex-col justify-between items-center min-h-screen min-w-screen">
<Header
user={session?.user}
locale={params.locale}
/>
{children}
<Footer
// locale={params.locale}
/>
<Toaster
// locale={params.locale}
/>
</main>
</ThemeProvider>
</body>
</html>
</NextAuthSessionProvider>
);
}
I thought I was doing it right regarding the Next.js docs. But I always receive this error log:
Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.
at AppContainer (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:341:29)
at AppContainerWithIsomorphicFiberStructure (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:377:57)
at div
at Body (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:677:21)
Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.
at AppContainer (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:341:29)
at AppContainerWithIsomorphicFiberStructure (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:377:57)
at div
at Body (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:677:21)
Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.
at PathnameContextProviderAdapter (C:\Users\samue\repos\app-template\node_modules\next\dist\shared\lib\router\adapters.js:78:11)
at AppContainer (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:341:29)
at AppContainerWithIsomorphicFiberStructure (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:377:57)
at div
at Body (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:677:21)
Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.
at PathnameContextProviderAdapter (C:\Users\samue\repos\app-template\node_modules\next\dist\shared\lib\router\adapters.js:78:11)
at AppContainer (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:341:29)
at AppContainerWithIsomorphicFiberStructure (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:377:57)
at div
at Body (C:\Users\samue\repos\app-template\node_modules\next\dist\server\render.js:677:21)
So I've tried different approach. But anything at best obtains me a "Cookies can only be modified in a Server Action or Route Handler"... But I AM using a server action!
I've checked the discussion on Next.js issue 51875, but I don't see how this helps me?
What am I doing wrong?
Upvotes: 3
Views: 9189
Reputation: 14067
you need to add "use server" inside the server action functions, e.g.:
import axios from "axios";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";
const Page = async ( ) => {
const getCSRFToken = async () => {
"use server";
try {
const {data, headers} = await axios.get( "http://backend:8000/api/auth/csrf/");
const csrfToken = data.csrf_token;
// or get it from headers['set-cookie'] but this needs to be parsed into object in specific form accepted by next.js cookie() api
// and also you need to make sure option keys are camelCase
// Set CSRF token as a cookie
cookies().set('csrftoken', csrfToken, { httpOnly: true })
return csrfToken;
} catch (error) {
console.error("Error fetching CSRF token:", error);
return null;
}
};
return (
<div>
<section>
<form
action={async () => {
"use server";
await getCSRFToken()
revalidatePath('/test-api')
redirect("/");
}}
>
<br />
<button type="submit">Login</button>
</form>
</section>
</div>
)
};
export default Page;
Upvotes: 0
Reputation: 51
Looking at the error log, the error is explanatory, but hard to debug with server actions. I've only ever seen this happen when trying to use server actions within a useEffect. There are a few things that I would suggest to try and solve/debug this issue.
The first would be to simply await your setting of the cookie.
await cookies().set({
name: "NEXT_LOCALE",
value: locale,
path: "/",
maxAge: 60 * 60 * 24 * 30, // 30 days
})
The second thought would be to make a useState and useEffect in LocaleSwitcher. Something along the lines of...
import { useState, useEffect } from "react"
const [locale, setLocale] = useState("");
useEffect(() => {
console.log(locale)
}, [locale])
This will break your application but you should then see the issue at hand. It will be log infinitely as if your state is being constantly updated which causes a rerender of your component.
From what I can see without running your code you essentially have the right parts. There is something within your application that is causing those rerenders. Without seeing your entire application, it's hard to debug. My only other thought is that the parameter locale, which you are passing in, is not a serialized value. Server actions must take in serialized arguments and return a serialized value.
Server actions are stable as of Next14 and do not require you to enable it as an experimental feature. It might be good to upgrade to 14.0.0 as there have been some bugs around server actions found in 14.0.2 but nothing major, that doesn't have a clear cut fix you can find on a GitHub gist. Next14 didn't make too many changes and you can check out the blog post to see if upgrading would break your application. https://nextjs.org/blog/next-14
"use server";
import { cookies } from "next/headers";
export async function create() {
const cookieStore = cookies();
const currentTheme = cookieStore.has("theme") ? cookieStore.get("theme").value : "dark";
const newTheme = currentTheme === "dark" ? "light" : "dark";
await cookieStore.set({
name: "theme",
path: "/",
value: newTheme,
});
}
"use client"
...
return (
<div>
<form action={create} >
<button type="submit" />
</form>
</div>
)
OR
<button onClick={() => {
create();
}}
>
Upvotes: 4