ui icon indicating copy to clipboard operation
ui copied to clipboard

[bug]: form.formState.isSubmitting not working in shadcn sonner

Open james-psi opened this issue 1 year ago • 1 comments

Describe the bug

await is not needed when I try to use toast.promise() in shadcn sonner, thats why the state in form.formState.isSubmitting is always false

Affected component/components

Sonner

How to reproduce

  1. Create a form uisng react-hook-form
  2. Create a form action with a toast.promise() to handle api response
  3. See that the supposedly disabled components like textfield, buttons, etc in the form while loading are still enabled since toast.promise() doesn't need to be marked with await

Minimal code to reproduce issue:

'use client'

import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form'

import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { toast } from 'sonner'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'

const formSchema = z.object({
  name: z.string().min(1, { message: 'Name is required' }),
})

export default function Sample() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: '',
    },
  })

  async function onSubmit(values: z.infer<typeof formSchema>) {
    toast.promise(
      new Promise<void>((resolve, reject) => {
        setTimeout(() => {
          resolve()
        }, 3000)
      }),
      {
        loading: 'Loading ...',
        success: () => {
          form.reset()
          return `Done! Name is ${values.name}`
        },
        error: (err) => {
          return `${err}`
        },
      },
    )
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input
                  disabled={form.formState.isSubmitting}
                  placeholder="Enter your name"
                  {...field}
                />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button disabled={form.formState.isSubmitting} type="submit">
          Submit
        </Button>
      </form>
    </Form>
  )
}

Codesandbox/StackBlitz link

No response

Logs

No response

System Info

Processor	Intel(R) Core(TM) i5-10300H CPU @ 2.50GHz   2.50 GHz
Installed RAM	24.0 GB (23.8 GB usable)
System type	64-bit operating system, x64-based processor
Pen and touch	No pen or touch input is available for this display

Browser: Brave, MS Edge

Before submitting

  • [X] I've made research efforts and searched the documentation
  • [X] I've searched for existing issues

james-psi avatar Jul 20 '24 13:07 james-psi

you need to do const { formState: { isSubmitting } } = useForm... in order to make it work as per the react hook form docs said:

Read the formState before render to subscribe the form state through the Proxy

but the approach in shadcn is const form = useForm... and use this form in <Form {...form}>... im trying to find a way to do this in shadcn too

richardluyangborja avatar Sep 05 '24 19:09 richardluyangborja

You can wrap toast.promise into a util function to make it return a Promise. Sonner sadly exports no types so you need to copy its type itself. I made success and error required because of resolve and reject

export function toastPromise<T>(
    promise: Promise<T>,
    args: {
        loading?: string | React.ReactNode;
        success: string | React.ReactNode;
        error: string | React.ReactNode;
        description?: string | React.ReactNode;
        finally?: () => void | Promise<void>;
    }
): Promise<T> {
    return new Promise((resolve, reject) => {
        toast.promise(promise, {
            ...args,
            success: (data) => {
                resolve(data);
                return args.success;
            },
            error: (err) => {
                reject(err);
                return args.error;
            }
        });
    });
}

Now instead of

   function onSubmit(values: z.infer<formSchema>) {
       toast.promise(
            new Promise((resolve) => setTimeout(resolve, 1000)),
            {
                loading: 'Saving...',
                success: 'Saved',
                error: 'Something went wrong'
            }
        );
    }

You can use

import { toastPromise } from '~/lib/utils';

 async function onSubmit(values: z.infer<formSchema>) {
      await toastPromise(
           new Promise((resolve) => setTimeout(resolve, 1000)),
           {
               loading: 'Saving...',
               success: 'Saved',
               error: 'Something went wrong'
           }
       );
   }

niebag avatar Oct 10 '24 13:10 niebag

This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)

shadcn avatar Oct 10 '25 23:10 shadcn

This issue has been automatically closed due to one year of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)

shadcn avatar Oct 17 '25 23:10 shadcn

is there now a better fix for this?

MoneyDropLobby avatar Nov 21 '25 15:11 MoneyDropLobby