admin管理员组文章数量:1202956
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?
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?
Share Improve this question asked Jun 2, 2023 at 22:57 Re-Angelo Re-Angelo 5372 gold badges8 silver badges23 bronze badges 3- how about you share some code first? – Yilmaz Commented Jun 2, 2023 at 23:00
- @Yilmaz There isn’t really any code for me to share. I was reading the documentation but didnt see anywhere how to go about doing that. – Re-Angelo Commented Jun 2, 2023 at 23:16
- Share the document u are reading – andsilver Commented Jul 12, 2023 at 1:30
4 Answers
Reset to default 9I 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`);
}
Short answer, there is no canonical way to do this. There are workarounds. Those could be different depending on if you are rendering a server or a client component, although they could be related.
- Option one: Set a cookie on the server action and read it on the next request (when the user is being redirected back to the page). You could set the cookie with a short expire time. You could put whatever you want in the cookie and render your toast based on it.
- Option two: Create a global state on the server and put whatever you want in it. You should identify the state you'll set with the identity of the request, otherwise you could be showing toasts to the wrong user.
- Option three: Use some kind of session manager and put the toast message on the session. Depending on the type of session you'll manage, this could be inside the header, the cookies, in memory, etc.
Honestly, it sucks that next doesn't offer a solution for this out of the box and leaves user to come up with whatever they can think of, including possible bad solutions.
See the cookie example:
// Server action file
"use server";
import { cookies } from "next/headers";
...
// At the end of your server action function:
cookies()
.set(
"my-app-toast-msg",
JSON.stringify({error: true, msg: "wrong credentials"}, {
expires: new Date(Date.now() + 10 * 1000), // 10 seconds
})
);
// At your server component file
import { cookies } from "next/headers";
export async function Page() {
const toastMsg = cookies().get("my-app-toast-msg");
return (
<>
{!!toastMsg &&
JSON.parse(toastMsg).error &&
<ClientErrorToast errorMsg={JSON.parse(toastMsg).msg} />
...
</>
)
}
This is a quick a dirty, non tested implementation from memory and I don't know if it's a good solution. I use an error as an example but you should be able to see the idea.
Building on Fabrizio Tognetto's Code. The idea is awesome, just one issue that when the user tries to submit the form again and the server action gets triggered, it doesn't give a toast, since the client component is looking at [props.flash]
as a state variable in the useEffect
block, and the flash
variable doesn't change since the same form message is being rendered. Thus, we can create a switch that toggles every time a new server action is triggered. I called it flashtrig
and modified Tognetto's code a bit.
flash-toaster-client
"use client"
import { useEffect } from "react"
import { toast } from "sonner"
export default function FlashToasterClient(props: {
flash: string | undefined
flashtrig: string | undefined
}) {
console.log(props.flashtrig)
useEffect(() => {
if (!!props.flash) {
console.log("this is that")
const { type, message } = JSON.parse(props.flash)
if (type === "success") {
toast.success(message)
} else if (type === "error") {
toast.error(message)
}
}
}, [props.flashtrig])
return null
}
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({ ...props }) {
const flash = cookies().get("flash")
const flashtrig = cookies().get("flashtrig")
return (
<>
<Toaster {...props} />
<FlashToasterClient flash={flash?.value} flashtrig={flashtrig?.value} />
</>
)
}
export function setFlash(flash: {
type: "success" | "error"
message: string
}) {
const flashtrig = cookies().get("flashtrig")
console.log(flashtrig?.value)
if (flashtrig?.value.startsWith("1")) {
console.log("1=>2")
cookies().set("flashtrig", JSON.stringify(2), {
path: "/",
expires: new Date(Date.now() + 100000),
})
} else {
console.log("2=>1")
cookies().set("flashtrig", JSON.stringify(1), {
path: "/",
expires: new Date(Date.now() + 100000),
})
}
cookies().set("flash", JSON.stringify(flash), {
path: "/",
expires: new Date(Date.now() + 1 * 1000),
})
}
index.ts
import { FlashToaster, setFlash } from "./flash-toaster"
export { FlashToaster, setFlash }
Use it in the same way the original answer states. Hope it helps.
本文标签: javascriptHow to call a notificationtoast after a server action in Nextjs13Stack Overflow
版权声明:本文标题:javascript - How to call a notificationtoast after a server action in Nextjs13 - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1738634067a2103919.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论