Introduction

Cookies are often used for tracking user behavior or store some data across different pages. In this article I will implement a cookie which helps me track the user activity and show according to the users interaction the right toast messages.


Cookie file

First I need to create a new server action which will enable me to interact with the cookies api of Next.js. Since I might will use these cookies across the whole application, I need to place it in a application wide actions folder:

src/actions/cookies.ts

The cookie will be made out of a key and a value. In the case of the toast message, the cookie can look like this:

key='toast', value='Ticket updated'

The cookie.ts file will enable me to the following:

  • set a cookie (key + value)
  • get a cookies value
  • delete a cookie
  • consume a cookie (gets cookie, returns value and deletes cookie again)
"use server";
 
import { cookies } from "next/headers";
 
export const setCookieByKey = async (key: string, value: string) => {
  (await cookies()).set(key, value);
};
 
export const getCookieByKey = async (key: string) => {
  const cookie = (await cookies()).get(key);
 
  if (!cookie) {
    return null;
  }
 
  return cookie.value;
};
 
export const deleteCookieByKey = async (key: string) => {
  (await cookies()).delete(key);
};
 
export const consumeCookiedByKey = async (key: string) => {
  const message = await getCookieByKey(key);
 
  await deleteCookieByKey(key);
 
  return message;
};

Redirect Toast Component

Although it would be technically possible to skip this component and place the useEffect hook directly in the corresponding page, doing so would require converting the entire page into a client-side component. Since the goal is to minimize client-side code as much as possible, it’s better to create a separate component. This way, only that specific component needs to run on the client, rather than the whole page.

This component can be placed here:

src/components/redirect-toast.tsx

The purpose of this component is to run a side effect every time it gets rendered. The side effect will consume the cookie (retrieve its value and delete it). If the message isn’t empty, it means there was a cookie, and the toast.success component will be shown.

"use client";
 
import { useEffect } from "react";
import { toast } from "sonner";
import { consumeCookiedByKey } from "@/actions/cookies";
 
const RedirectToast = () => {
  useEffect(() => {
    const showCookieToast = async () => {
      const message = await consumeCookiedByKey("toast");
 
      if (message) {
        toast.success(message);
      }
    };
 
    showCookieToast();
  }, []);
 
  return null;
};
 
export { RedirectToast };
 

Since this component only purpose is to run the side effect, it does not need to return anything but null.


Set the Cookie

Depending on the user action, I want to set a cookie. In my application, there are two use cases:

A) The user deletes a ticket.

// src/features/ticket/actions/delete-ticket.ts
 
export const deleteTicket = async (id: string) => {
  // ...
 
  revalidatePath(ticketsPath());
  await setCookieByKey("toast", "Ticket deleted");
  redirect(ticketsPath());
};
 

B) The user updates a ticket.

// src/features/ticket/actions/upsert-ticket.ts
 
export const upsertTicket = async () => {
  // ...
 
  revalidatePath(ticketsPath());
 
  if (id) {
    await setCookieByKey("toast", "Ticket updated");
    redirect(ticketPath(id));
  }
};

Show the toast message

Important: This side effect only ever runs when redirected and therefore got an path change, not when revalidated.

The basic logic for showing the toast message is already implemented in the Redirect Toast Component, but now I need to make sure it gets executed when navigated.

For that, I need the component to have an eye on the pathname, I can do this via the dependency list:

const RedirectToast = () => {
 
  // Let the component to get the current path
  const pathname = usePathname();
 
  useEffect(() => {
     // track the path for changes
  }, [pathname]);
};
 
 

Template

The above mentioned step with watching the pathname, should not be needed when using template.js. Templates should be automatically remount on navigation. However there seems to be currently a bug with that behavior, so as of now I still need the work around with the pathname.

I still can create a RootTemplate here:

src/app/template.tsx

And add this code:

import { RedirectToast } from "@/components/redirect-toast";
 
type RootTemplateProps = {
  children: React.ReactNode;
};
 
export default function RootTemplate({ children }: RootTemplateProps) {
  return (
    <>
      <>{children}</>
      <RedirectToast />
    </>
  );
}
 

Now the template automatically gets wrapped between the layout.tsx and the page.tsx.

<RootLayout>
  <Template key="/">
    <Page />
  </Template>
</RootLayout>

Since I added the RedirectToast component in the RootTemplate, it will be accessible to the whole application.


Conclusion