ui icon indicating copy to clipboard operation
ui copied to clipboard

Select with react-hook-form Control

Open rogueturnip opened this issue 2 years ago • 17 comments

Hi! I'm having a bit of an issue with the Select setup using react-hook-form.

What I'm trying to do is display a form with default values, there is a button to populate the content from an api. When this api is called all the fields in the form should update to the data from the api.

This works fine for all the fields except for Select. I set it up with a plain html select and it's working but I can't seem to get it to work with the Select component from shadcn.

Here is a snip of the parts I'm confused by.

Here is the form setup

  const npcForm = useForm<z.infer<typeof npcFullRandomZodSchema>>({
    resolver: zodResolver(npcFullRandomZodSchema),
    values: npc,
    defaultValues: {
      class: 'none',

    },
    resetOptions: {},
  });

Here is the Select setup:
      <Controller
        name="class"
        control={npcForm.control}
        render={({ field }) => (
          <FormItem>
            <FormLabel>Class</FormLabel>
            <Select
              onValueChange={field.onChange}
              defaultValue={field.value}
            >
              <FormControl>
                <SelectTrigger>
                  <SelectValue />
                </SelectTrigger>
              </FormControl>
              <SelectContent ref={field.ref}>
                {dnd5eClasses
                  .sort((a, b) => a.label.localeCompare(b.label))
                  .map((option) => (
                    <SelectItem key={option.value} value={option.value}>
                      {option.label}
                    </SelectItem>
                  ))}
              </SelectContent>
            </Select>
          </FormItem>
        )}
      />

And here is the html select that works.

          <Controller
            name="class"
            control={npcForm.control}
            render={({ field }) => (
              <select {...field}>
                {dnd5eClasses
                  .sort((a, b) => a.label.localeCompare(b.label))
                  .map((option) => (
                    <option key={option.value} value={option.value}>
                      {option.label}
                    </option>
                  ))}
              </select>
            )}
          />

I've tried using FormField in addition to just the controller but can't figure out what I'm missing.  I'm sure it's simple but there isn't anything I can find related in docs to help.

Thanks!

rogueturnip avatar Aug 16 '23 13:08 rogueturnip

I tested with react-select and it works also. So it's either a bug in the Select or I'm not sure where the "ref" goes :)

rogueturnip avatar Aug 16 '23 14:08 rogueturnip

Thanks for this tip. I am also facing a similar problem. Everything works for me, but it seems each time I select a value, it is not showing in the SelectValue control. It is just showing empty. It will only work if I manually provided the SelectItem manually without using dynamically generated values. Your solution seems to work for now. I hope this issue will be fixed soon because now I have an odd control that does not match the overall theme of the app.

yousihy avatar Sep 06 '23 15:09 yousihy

@rogueturnip try use value={field.value} instead of defaultValue={field.value}

joaom00 avatar Sep 06 '23 16:09 joaom00

@rogueturnip try use value={field.value} instead of defaultValue={field.value}

I did. It's not working. Can you check my code sample? sample

yousihy avatar Sep 06 '23 16:09 yousihy

Hey @rogueturnip, if your issue is the same as @yousihy, make sure the value prop of SelectItem is a string Codesandbox

joaom00 avatar Sep 06 '23 16:09 joaom00

I'm having the same issue with Input. It doesn't catch the register field result.

Simply, I can't do this:

<Input {...register("email")} />

UPDATE

I figured the problem. I must use RHF Controller component to make custom inputs work below it.

adnanalbeda avatar Jan 25 '24 23:01 adnanalbeda

can u share how did you used RHF Controller component please

leandiazz avatar Mar 15 '24 21:03 leandiazz

Here's how i solved this issue:

  1. create a state that will notify that you already updated the form
const [isDone, setIsDone] = useState(false);
  1. After you reset/update the form values, update the state
reset((v) => ({
        ...v,
        productType: `${product.product_type}`,
        productName: `${item.id}`,
        price: `${product.price}`,
        code: `${product.code}`,
        stocks: `${product.stocks}`,
        stocksWarning: `${product.stocks_warning}`,
        weight: `${product.weight}`,
        unit: `${product.unit}`,
        vatable: `${product.vatable}`,
        description: `${product.description}`,
        status: `${product.status}`,
      }));
      setIsDone(true);
  1. on your form element add key:
<form key={isDone ? 0 : 1} onSubmit={handleSubmit(onSubmit)}>

This will force re-render the form element together with the select elements that are not updating

nelwincatalogo avatar Mar 21 '24 04:03 nelwincatalogo

can u share how did you used RHF Controller component please

Same way described in the docs.

See the example section.

adnanalbeda avatar Apr 01 '24 13:04 adnanalbeda

I'm having the same issue. I'm pulling in data and using that to populate various form values. Input and Switch work fine, select does not.

Essentially, I'm able to dynamically update the values of the other fields based on my data. However, Select doesn't update.. it just goes back to it's original state.

I’m encountering an issue with the Select component for the type field. The form correctly retrieves initial values from session storage and uses form.reset to set these values. While form.reset works for setting initial values of other components like Input and Switch, the Select component’s value does not persist and resets to an empty string. Manually setting the value with form.setValue() works for other components but not for the Select component. Despite seeing the correct value being loaded and set in the console logs, the type field value unexpectedly clears, causing inconsistencies in the form state.

I've tried defaultValue={field.value} and value={field.value}

// Create form schema using Zod
const formSchema = z.object({
    name: z.string().min(1, { message: "Name must be at least 1 character long" }),
    type: z.string().min(1),
    notifications: z.boolean()
});

export type FormSchema = z.infer<typeof formSchema>;

// Set Session Storage:
const setSessionStorage = (key: string, formData: FormSchema) => {
    console.log("Storing data in session storage:", formData);
    sessionStorage.setItem(key, JSON.stringify(formData));
};

// Get Session Storage
const getSessionStorage = (key: string) => {
    const data = sessionStorage.getItem(key);
    console.log("Retrieved data from session storage:", data);
    return data ? JSON.parse(data) : null;
};

// OnboardingForm Component
const OnboardingForm = ({ onSubmit }: { onSubmit: (data: FormSchema) => void }) => {
    const [sessionFormData, setSessionFormData] = useState<FormSchema>({
        name: '',
        type: '',
        notifications: false,
    });

    // Create form object and initialize default values using session storage or blank object.
    const form = useForm<FormSchema>({
        resolver: zodResolver(formSchema),
        defaultValues: sessionFormData,
    });

    // Fetch session storage data on mount
    useEffect(() => {
        const storedData = getSessionStorage('onboardingFormData');
        console.log("Retrieved stored data on mount:", storedData);
        if (storedData) {
            setSessionFormData(storedData);
            console.log("Calling form.reset with:", storedData);
            form.reset(storedData); // Reset form with retrieved data
        }
    }, [form]);

    console.log(sessionFormData)
    

    // Watch the form for any input changes and update SessionStorage
    const formValues = useWatch({
        control: form.control,
    });

    useEffect(() => {
        const formData = form.getValues();
        console.log("Form Values Changed:", formData);
        setSessionStorage('onboardingFormData', formData);
    }, [formValues]);

    return (
        <Card className="mx-auto w-full">
            <CardHeader>
                <h1 className='text-3xl font-semibold'>Welcome!</h1>
                <CardDescription>
                    Let's get some details and then we'll create your account
                </CardDescription>
            </CardHeader>
            <CardContent>
                <Form {...form}>
                    <form className="w-full space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
                        <FormField
                            control={form.control}
                            name="name"
                            render={({ field }) => (
                                <FormItem>
                                    <FormLabel className='pl-2'>Name</FormLabel>
                                    <FormControl>
                                        <Input className='w-full' placeholder="Mo Tanveer" {...field} />
                                    </FormControl>
                                    <FormMessage />
                                </FormItem>
                            )}
                        />

                        <FormField
                            control={form.control}
                            name="type"
                            render={({ field }) => (
                                <FormItem>
                                    <FormLabel className='pl-2'>License Type</FormLabel>
                                    <Select value={field.value} onValueChange={field.onChange}>
                                        <FormControl>
                                            <SelectTrigger >
                                                <SelectValue  placeholder="Select your license type..." />
                                            </SelectTrigger>
                                        </FormControl>
                                        <SelectContent>
                                            <SelectItem value="Road">Road License</SelectItem>
                                            <SelectItem value="Circuit">International Circuit License</SelectItem>
                                            <SelectItem value="Rally">International Rally License</SelectItem>
                                        </SelectContent>
                                    </Select>
                                    <FormMessage />
                                </FormItem>
                            )}
                        />

                        <FormField
                            control={form.control}
                            name="notifications"
                            render={({ field }) => (
                                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
                                    <div className="space-y-0.5">
                                        <FormLabel>
                                            Notifications
                                        </FormLabel>
                                        <FormDescription>
                                            Receive Race Event Notifications?
                                        </FormDescription>
                                    </div>
                                    <FormControl>
                                        <Switch
                                            checked={field.value}
                                            onCheckedChange={field.onChange}
                                        />
                                    </FormControl>
                                </FormItem>
                            )}
                        />
                        <Button className='w-full' type="submit">Next</Button>
                    </form>
                </Form>
            </CardContent>
        </Card>
    );
};

motanveer avatar Jun 26 '24 02:06 motanveer

use:
<Select onValueChange={field.onChange} {...field}> .... </Select>

You can see this in StackOverFlow 

https://stackoverflow.com/questions/75815473/how-can-i-implement-react-hook-form-with-radix-ui-select

ENGAhmedAbdelfattah avatar Jun 27 '24 04:06 ENGAhmedAbdelfattah

Here's how i solved this issue:

  1. create a state that will notify that you already updated the form
const [isDone, setIsDone] = useState(false);
<form key={isDone ? 0 : 1} onSubmit={handleSubmit(onSubmit)}>

This will force re-render the form element together with the select elements that are not updating

in this case use key={form.watch(<FIELDNAME>)} instead of declaring a separate state

k4l3b4 avatar Jul 10 '24 21:07 k4l3b4

I understand there are likely work-arounds to this, and it may not even be a bug at all, but user implementation. But for anyone finding this thread I wanted to say I'm experiencing a very similar situation to motanveer above .. In my case however I'm getting the empty string value for the Select value when I navigate back and retrieve the values out of a form.reset() . He's getting his previously entered form values from local / sesh storage and I'm pulling mine out of a context-store and then form.reset(contextFormObj) and it's doing the same confounding thing where all the other shad-cn components like Input or Radio or any other ones (oh by the way .. THANK YOU @shadcn for this exceptionally awesome library) repopulate just fine .. but just the single lone Select value is an empty string .. even though I can see it's value is submitted correctly and is stored in context correctly (Meaning this is really purely a UI issue where we want to show the user that indeed that value is there to their own eyes).

stall84 avatar Jul 20 '24 19:07 stall84

Yeah , i am also encountering a similar issue and unable to make it work. I am using react query to fetch data and i can see all the API fields are populated but when i call form.reset() with the result from the API, the form fields which have select as input contain empty strings, but the rest of the form gets populated.

Tweedle2Dum avatar Jul 22 '24 07:07 Tweedle2Dum

I encountered this problem too. It seems that something is firing the onValueChange event with an empty string when the route changes. I just checked if the value being passed onValueChange is an empty string before updating the value. Hopefully we can get a better solution in the future. <ShadSelect disabled={!!disabled} onValueChange={(val: string) => { if (val) onChange(val); }} value={value} > </ShadSelect>

Oladejiraji avatar Jul 24 '24 19:07 Oladejiraji

Specifying key={field.value} solved the issue for me.

 <FormField
      control={control}
      name={name}
      render={({ field }) => {
        return (
          <FormItem key={field.value} className="w-full">
            <FormLabel>{label}</FormLabel>
            <Select
              onValueChange={(value) => field.onChange(Number(value))}
              value={String(field.value)}
            >
              <FormControl>
                <SelectTrigger>
                  <SelectValue placeholder={placeholder} />
                </SelectTrigger>
              </FormControl>
              <SelectContent>
                {options.map((option) => (
                  <SelectItem key={option.id} value={option.id.toString()}>
                    {option.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            <FormMessage />
          </FormItem>
        );
      }}
    />

abhishek-butola avatar Jul 30 '24 05:07 abhishek-butola

if i use "" as default value, the first options is always selected when submit with form element before I made a selection Screenshot 2024-08-10 at 01 41 30

tsulatsitamim avatar Aug 09 '24 18:08 tsulatsitamim

Specifying key={field.value} solved the issue for me.

 <FormField
      control={control}
      name={name}
      render={({ field }) => {
        return (
          <FormItem key={field.value} className="w-full">
            <FormLabel>{label}</FormLabel>
            <Select
              onValueChange={(value) => field.onChange(Number(value))}
              value={String(field.value)}
            >
              <FormControl>
                <SelectTrigger>
                  <SelectValue placeholder={placeholder} />
                </SelectTrigger>
              </FormControl>
              <SelectContent>
                {options.map((option) => (
                  <SelectItem key={option.id} value={option.id.toString()}>
                    {option.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            <FormMessage />
          </FormItem>
        );
      }}
    />

This fixed the issue for me as well.

stevensiht avatar Aug 12 '24 17:08 stevensiht