Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call a notification/toast after a server action in Nextjs13

Hi I am struggling to find out how to call a toast after a successful data mutation using server actions in Nextjs 13. The toast is a client component since it uses a hook/context.

How would I go about doing this?

like image 500
Re-Angelo Avatar asked Dec 07 '25 19:12

Re-Angelo


2 Answers

I was able to achieve this result using the useFormStatus() hook.

Suppose you have a form component that uses a Server Action. Here's a basic Server Component that might invoke your server action, defined in another file:

import { submit } from "./serverAction";

export function ServerForm() {
  return (
    <form action={submit}>
      <input type="email" name="email" />
      <input type="text" name="name" />
      <input type="submit" />
    </form>
  );
}

To manage data returned by the server action and, for instance, display errors adjacent to each input, you'd need to convert this form into a Client Component:

"use client"

import { submit } from "./serverAction";
// @ts-ignore: Experimental feature not yet in react-dom type definitions
import { experimental_useFormState as useFormState } from "react-dom";

export function ClientForm() {
  // You pass your ServerAction and an initial state to `useFormState`
  const [state, formAction] = useFormState(submit, {
    error: { name: null, email: null },
    message: null,
  });

  return (
    <form action={formAction}>
      <input type="email" name="email" />
      <p>{state?.error?.email}</p>
      <input type="text" name="name" />
      <p>{state?.error?.name}</p>
      <input type="submit" />
    </form>
  );
}

In this context, I'm returning an object from my ServerAction:

"use server";

export async function submit(formData: FormData) {
  // Extract data from the submitted form
  const name = formData.get("name") as string;
  const email = formData.get("email") as string;

  try {
    // You might do data validation and database interactions here
    // On success, you'd probably redirect instead of returning an object
    return { error: null, message: `New submission: ${name} - ${email}` };
  } catch (e) {
    // Error handling
    return {
      error: {
        name: "There was an error with this name",
        email: "There was an error with this email",
      },
      message: "Failed submission",
    };
  }
}

You can seamlessly trigger a Toaster by setting a useEffect that observes state. For instance, using the toaster from shadcn:

"use client";

import { useToast } from "@components/shadcn-ui-registry/use-toast";
import { useEffect } from "react";
import { submit } from "./serverAction";
// @ts-ignore: Experimental feature not yet in react-dom type definitions
import { experimental_useFormState as useFormState } from "react-dom";

export function ClientForm() {
  // You pass your ServerAction and an initial state to `useFormState`
  const [state, formAction] = useFormState(submit, {
    error: { name: null, email: null },
    message: null,
  });

  const { toast } = useToast();
  useEffect(() => {
    toast({
      title: state?.error?.name || state?.error?.email ? "Error" : "Success",
      description: state.message,
    });
  }, [state, toast]);

  return (
    <form action={formAction}>
      <input type="email" name="email" />
      <p>{state?.error?.email}</p>
      <input type="text" name="name" />
      <p>{state?.error?.name}</p>
      <input type="submit" />
    </form>
  );
}

For more details, refer to the Next.js documentation for forms and mutations. Hope this helped, feel free to give feedback or ask anything that was unclear.

like image 96
fig Avatar answered Dec 10 '25 12:12

fig


I have made a little library to handle this, working with nextjs and shadcn, coming from SvelteKit where there is the flash-message library https://github.com/ciscoheat/sveltekit-flash-message

Put this in your lib/flash-toaster

flash-toaster.tsx

import { Toaster } from '@/components/ui/sonner';
import FlashToasterClient from '@/lib/flash-toaster/flash-toaster-client';
import { cookies } from 'next/headers';

export function FlashToaster() {
  const flash = cookies().get('flash');
  return (
    <>
      <Toaster />
      <FlashToasterClient flash={flash?.value} />
    </>
  );
}

export function setFlash(flash: { type: 'success' | 'error'; message: string }) {
  cookies().set('flash', JSON.stringify(flash), { path: '/', expires: new Date(Date.now() + 10 * 1000) });
}

flash-toaster-client.tsx

'use client';
import { useEffect } from 'react';
import { toast } from 'sonner';

export default function FlashToasterClient(props: { flash: string | undefined }) {
  useEffect(() => {
    if (!!props.flash) {
      const { type, message } = JSON.parse(props.flash);
      if (type === 'success') {
        toast.success(message);
      } else if (type === 'error') {
        toast.error(message);
      }
    }
  }, [props.flash]);
  return null;
}

index.ts

import { FlashToaster, setFlash } from './flash-toaster';
export { FlashToaster, setFlash };

Then in your layout.tsx (where you would put your <Toaster/> component) use the <FlashToaster /> like this

app/layout.tsx

import { FlashToaster } from '@/lib/flash-toaster';
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head />
      <body>
        <main>{children}</main>
        <FlashToaster />
      </body>
    </html>
  )
}

And in your actions you can call the setFlash function to add messages that will persist across redirects

action.ts


export function createUser() {
  ... create the user ...

  setFlash({ type: 'success', message: 'User create successfully' });
  redirect(`/users`);
}
like image 30
Fabrizio Tognetto Avatar answered Dec 10 '25 10:12

Fabrizio Tognetto



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!