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?
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.
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`);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With