How toShow toast after a Remix action

This is a really common scenario, you have a route with a form inside, the user fills the form, submit it and then your action runs.

After that, you want to show some toast to let the user know everything worked, maybe also redirect to another URL or keep the user in the same route and return some result data to update the UI.

The most common way to do this is to use the useActionData hook to get the result data and then render a toast.

let actionData = useActionData<typeof action>();
if (actionData?.ok) render <Toast />;

Another common way is to use an effect to call some function to trigger the toast, and probably also use a context to access the toast function.

let toast = useToast()
let actionData = useActionData<typeof action>();
useEffect(() => {
  if (actionData?.ok) toast();
}, [actionData, toast]);

The problem here is that now we will need to ensure that every actionData has some unique value so our effect runs again.

And if we just render it, we may found that actionData is not cleared until the user navigate somewhere else so the Toast will remain rendered, and even if we hide it after a certain time it may render again if another state trigger a re-render.

What we really want is a place where we can trigger a toast function only once after the action run, and where we can be sure the function will run regardless of the uniqueness of the value.

Well there's a place in Remix to do that, it's the clientAction route function.

The clientAction takes the place of our (server) action and handles any form non-GET submission in the route.

This function can call the server action if needed, so what we can do is to combine them.

export async function clientAction({ serverAction }: ClientActionFunctionArgs) {
  return serverAction<typeof action>().finally(() => {
    toast("User created!");
  });
}

This clientAction will immediately call the serverAction function it receives and once the server action ends it will call our toast function.

This will even work if the serverAction throw a redirect response, as the finally will run for a thrown value too.

If we want more control, we could always return data and never a redirect, then use that data to show the toast and maybe redirect.

export async function clientAction({ serverAction }: ClientActionFunctionArgs) {
  let data = await serverAction<typeof action>();

  // On error, send data to show error UI
  if (!data.ok) return data;
  // On success, trigger toast and redirect
  toast("User created!")
  return redirect(`/users/${data.id}`)
}