reactjstypescriptnext.jsnext.js13toast

react-hot-toast shows duplicate toasts


import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
// import ToastProvider from "./(components)/Toastify";
import { Toaster } from "react-hot-toast";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        {children}
        <Toaster />
      </body>
    </html>
  );
}
"use client";
import { getDebugger } from "@/app/lib/debugger";
import { registerNewUser } from "@/app/lib/actions";
import FormInput from "../../(components)/forms/FormInput";
import SubmitBtn from "../../(components)/forms/SubmitBtn";
// import { toast } from "react-toastify";
import toast from "react-hot-toast";
import { useFormState } from "react-dom";

const debug = getDebugger("sign-up-form");

const initialState = {
  message: null,
};

const Page = () => {
  const [state, formAction] = useFormState(registerNewUser, initialState);
  return (
    <main>
      <form action={formAction}>
        <FormInput
          name="firstName"
          label="First Name:"
          type="text"
          placeholder="Your First Name"
        />
        <FormInput
          name="lastName"
          label="Last Name:"
          type="text"
          placeholder="Your Last Name"
        />
        <FormInput
          name="password"
          label="Password:"
          type="password"
          placeholder="******"
        />
        <FormInput
          name="confirmPassword"
          label="Confirm Password:"
          type="password"
          placeholder="******"
        />
        <FormInput
          name="email"
          label="Email:"
          type="email"
          placeholder="Your Email Here"
        />
        <SubmitBtn text="Sign Up" />
        {state?.message && toast.error(state.message)}
      </form>
    </main>
  );
};
export default Page;
"use server";
import { customFetch } from "./customFetch";
import { getDebugger } from "./debugger";

const debug = getDebugger("actions");

export const registerNewUser = async (prevState: any, formData: FormData) => {
  const user = {
    firstName: formData.get("firstName") as string,
    lastName: formData.get("lastName") as string,
    password: formData.get("password") as string,
    confirmPassword: formData.get("confirmPassword") as string,
    email: formData.get("email") as string,
  };

  try {
    const { data } = await customFetch.post("/api/auth/sign-up", user);
    return data;
  } catch (error: any) {
    return { message: error.response.data.error };
  }
};

screenshot of duplicate toasts

In the screenshot above, note the number next to the button and the multiple times that the toast is displaying.

I'm trying to implement toastify inside my NextJS app, but for some reason I'm getting more than one toast displaying.

I'm also seeing a number next to my button (see image), and every time I click the sign up button, the number increases by 1, 2, or 3 (depends on the number of toasts displaying).

Here is the GitHub repo.


Solution

  • That's because {state?.message && toast.error(state.message)}

    You must not put any execuatable code withen JSX. JSX is recreated every time the component re-renders.

    You have two issues in the code:

    1. The logical and &&

    When using logical and && in JSX, It'll check the left side if it is true, it will print the right side. toast.error(...) returns the id of the toast (number) so it's printed in the dom as you can see. It's increased because every time the component re-renders it creates new toast with new id.

    1. Execuatble code in JSX

    You must never call any function other than components withen JSX.

    To achieve what you are looking for, you can use useEffect

    useEffect(() => {
      if (state.message) toast.error(state.message)
    }, [state.message])