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
Add a comment  | 

4 Answers 4

Reset to default 9

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`);
}

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