Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React error "uncontrolled input" in forms (demo repo provided) #410

Closed
Zwyx opened this issue May 22, 2023 · 27 comments
Closed

React error "uncontrolled input" in forms (demo repo provided) #410

Zwyx opened this issue May 22, 2023 · 27 comments

Comments

@Zwyx
Copy link
Contributor

Zwyx commented May 22, 2023

Hi again Shadcn and congrats for continuing to improve your already awesome project!

I noticed that React throws the error A component is changing an uncontrolled input to be controlled when we start typing in a form input:

image

Here is a fresh Next.js project to which I added the example form provided in the doc:
https://github.com/Zwyx/shadcn-ui-form-uncontrolled-input-demo

Simply start the project, open your browser's console, and type in the form. You should see the error being thrown.

@murnifine
Copy link

same problem

@shadcn
Copy link
Collaborator

shadcn commented May 22, 2023

So, since FormField is using a controlled component, you need to provide a default value for it. Otherwise React is seeing it going from undefined to a value.

const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  defaultValues: {
+    username: "",
  },
})

From the docs:

Important: Can not apply undefined to defaultValue or defaultValues at useForm.You need to either set defaultValue at the field-level or useForm's defaultValues. undefined is not a valid value.If your form will invoke reset with default values, you will need to provide useForm with defaultValues.

(Edit: I've update the example in the docs)

@Zwyx
Copy link
Contributor Author

Zwyx commented May 22, 2023

Thank you very much 🙂

@Wouter8
Copy link

Wouter8 commented Jul 7, 2023

I have a number input, so I can't set the default value to an empty string.
Any workaround for this?

@dan5py
Copy link
Contributor

dan5py commented Jul 7, 2023

@Wouter8 you can set a number as default value.

@Wouter8
Copy link

Wouter8 commented Jul 7, 2023

@Wouter8 you can set a number as default value.

@dan5py I don't want to set an arbitrary default value like 0...

@Zwyx
Copy link
Contributor Author

Zwyx commented Jul 7, 2023

I think an input of type="number" still accepts and returns a string as value? It seems like an empty string is the way to give a default value when you want an empty number field: facebook/react#7779

@Wouter8
Copy link

Wouter8 commented Jul 10, 2023

I can't manually provide the value since it is being controlled (implementation of FormField).

I can't apply an empty string in the defaultValues in the useForm call.

const formSchema = z.object({
  number: z.number().int().min(1),
});

export default function Component() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      number: "",
    },
  });

Type 'string' is not assignable to type 'number'.

Setting value explicitly to 0:
<Input placeholder="382201" value={0} {...field} />
'value' is specified more than once, so this usage will be overwritten.ts(2783) page.tsx(64, 59): This spread always overwrites this property.

This does not solve the issue since value={0} is overwritten with value={undefined} from the field spread.

@Zwyx
Copy link
Contributor Author

Zwyx commented Jul 10, 2023

Hm... it seems like you would need to, either:

  • have a string in your form, you would then convert it to a number when you save the form data (you can still have type="number" on the input, so the browser prevents the user from entering non-number characters);
  • or to have a number in your form, but with a default value.

@bennik88
Copy link

bennik88 commented Jul 22, 2023

I don't really understand this behavior. I think it's counter-intuitive to have a default value as a must in this case. I have a form and a select with multiple choices inside and I want to show the placeholder by default and not a selected value. Let's say the selector is gender and "male", "female" ... . I don't want to specify a gender by default. But as my Form is used with my type formSchema a null value or empty string would cause a typescript error. And accepting that in my validator would cause my validation to allow null values which I don't want. So to get rid of it I would need to set an enum value from gender, so "male" as a default value? How are you getting around this issue? Am I missing something?

@evanlong0803
Copy link

evanlong0803 commented Jul 26, 2023

So, since FormField is using a controlled component, you need to provide a default value for it. Otherwise React is seeing it going from undefined to a value.

const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  defaultValues: {
+    username: "",
  },
})

From the docs:

Important: Can not apply undefined to defaultValue or defaultValues at useForm.You need to either set defaultValue at the field-level or useForm's defaultValues. undefined is not a valid value.If your form will invoke reset with default values, you will need to provide useForm with defaultValues.

(Edit: I've update the example in the docs)

The question is how do I check that my field is an empty string?

@evanlong0803
Copy link

evanlong0803 commented Jul 26, 2023

So, since FormField is using a controlled component, you need to provide a default value for it. Otherwise React is seeing it going from undefined to a value.

const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  defaultValues: {
+    username: "",
  },
})

From the docs:

Important: Can not apply undefined to defaultValue or defaultValues at useForm.You need to either set defaultValue at the field-level or useForm's defaultValues. undefined is not a valid value.If your form will invoke reset with default values, you will need to provide useForm with defaultValues.

(Edit: I've update the example in the docs)

And required_error of z.string({required_error: "xxxx"}) will only be triggered if the default is undefined

@evanlong0803
Copy link

I used it to trigger required_error

onChange={(event) => {
 field.onChange(event.target.value || undefined)
}}

@tarikyildizci
Copy link

<Input {...field} type="number" min={0} value={field.value ?? ""} />

this seems to resolve this issue, hook-form and zod still see the field.value, which is undefined, but the component recieves an empty string instead of undefined.

@evanlong0803
Copy link

<Input {...field} type="number" min={0} value={field.value ?? ""} />

this seems to resolve this issue, hook-form and zod still see the field.value, which is undefined, but the component recieves an empty string instead of undefined.

What are your default values?

@tarikyildizci
Copy link

<Input {...field} type="number" min={0} value={field.value ?? ""} />
this seems to resolve this issue, hook-form and zod still see the field.value, which is undefined, but the component recieves an empty string instead of undefined.

What are your default values?

for hook-form, undefined, but the input is tricked into it being an empty string

@JohnGemstone
Copy link

Thanks for this @tarikyildizci, helps get around the default values tripping zod for required inputs. This must be a very common problem, it's the most basic implementation of a form, this should have more attention imo.

@nmerchant
Copy link

nmerchant commented Oct 3, 2023

Thank goodness I ran across this thread, so I'm not totally crazy. It's kinda blowing my mind that very straightforward and simple use cases like this aren't working out of the box.

Thanks for this @tarikyildizci, helps get around the default values tripping zod for required inputs. This must be a very common problem, it's the most basic implementation of a form, this should have more attention imo.

I'm seeing an issue with this solution when I use a simple validation like z.string({ required_error: "Name is required." }),
If I try to submit my form without entering anything the field shows that validation message as expected. But if I enter a value and then delete it and hit submit again, it doesn't recognize the field as empty.
I have to add a separate validation rule .min(1, "Name is required") for it work:

z.string({ required_error: "Name is required." }).min(1, "Name is required."),

It's also unclear to me how this should work in the cases of other input types, like dates, or something like a text input where I want to coerce the value to a date or number (i.e. DOB).

Surely there's a straightforward solution to all this somewhere?

@mununki
Copy link

mununki commented Oct 30, 2023

Can I use the uncontrolled component with react-hook-form? If so, we can implement it:

const formSchema = z.object({
  totalArea: z.union([z.number().int().positive().min(1), z.nan()]).optional(),
})

const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  defaultValues: {
    // no default value for totalArea
  },
})

<FormField
  control={form.control}
  name="totalArea"
  render={({ field }) => (
    <FormItem>
      <FormLabel> 재배면적</FormLabel>
        <FormControl>
          <Input type="number"
            // using uncontrolled component with react-hook-form
            {...form.register("totalArea", { setValueAs: v => v === "" ? undefined : parseInt(v, 10) })}
          />
        </FormControl>
        <FormMessage />
    </FormItem>
  )}
/>

It works fine to me. But I'm still not sure if I'm missing some features from <FormControl /> or <FormField.control />

@marcollilorenzo
Copy link

I used it to trigger required_error

onChange={(event) => {
 field.onChange(event.target.value || undefined)
}}

This for me work!

@alitasofficial
Copy link

alitasofficial commented Feb 8, 2024

@Wouter8 you can set a number as default value.

@dan5py I don't want to set an arbitrary default value like 0...

You can use zod coerce method and set the default value of the number to empty string.
-> z.coerce.number().min(0),

Downsides

  1. Then you will see a typescript error but solves the problem anyway makes sure you always only receive a number input.
  2. User is allowed to put string in the number input field and you warn them with an error message (not the best UX but saves the day)

WhatsApp Image 2024-02-08 at 22 13 11

@blackmax1886
Copy link

blackmax1886 commented Feb 21, 2024

I can't manually provide the value since it is being controlled (implementation of FormField).

I can't apply an empty string in the defaultValues in the useForm call.

const formSchema = z.object({
  number: z.number().int().min(1),
});

export default function Component() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      number: "",
    },
  });

Type 'string' is not assignable to type 'number'.

Setting value explicitly to 0: <Input placeholder="382201" value={0} {...field} /> 'value' is specified more than once, so this usage will be overwritten.ts(2783) page.tsx(64, 59): This spread always overwrites this property.

This does not solve the issue since value={0} is overwritten with value={undefined} from the field spread.

I have same issue and I wonder any solution for default value that is not empty string & arbitrary value in optional number field?

@martindanielson
Copy link

Also interested in this, trying to have number validation in a form. I simply cannot get it to work and am falling back to very hacky solutions validating on later on before storing the data but would be nice to have this working out of the box.

Tagging this; will dig a little more and see if I come up with something.

@BrendanC23
Copy link

I've implemented numbers in Zod using the following:

function requiredNumberTransform(num: number | "", ctx: z.RefinementCtx) {
    if (num === "") {
        ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: "Enter a number",
            fatal: true,
        });

        return z.NEVER;
    }

    return num;
}

const OptionalNumber = z.number().or(z.literal(""));
const RequiredNumber = OptionalNumber.transform(requiredNumberTransform);

This works with z.input<typeof schema> and z.output<typeof schema> and the number fields will be properly typed. You can pass an empty string as a default value, and the form's submission type will be number for RequiredNumber or number | "" for OptionalNumber.

@stathis1998
Copy link

I used it to trigger required_error

onChange={(event) => {
 field.onChange(event.target.value || undefined)
}}

This for me work!

I think you can use the form.resetField("username") and that will still consider it untouched ?

@GustavoOS
Copy link

GustavoOS commented Nov 23, 2024

So for a controlled input, there is no way to only validate values that the user has entered itself?
Here are 2 downsides

  1. No placeholders
    Let's say I want to validate a phone number of a form, for instance. I can use something like libphonenumber-js with some fancy zod rules to check if the phone number is valid.
    If I go with controlled inputs, I have to provide a initial value (so no placeholder will be shown) that would be a valid phone

  2. Types limited to string
    Let's say I want to convert the date into a number that represents the season (doable using zod's transform property).
    If I go with controlled inputs, I can't provide a initial value that is neither a number nor a date

Etsija added a commit to eclipse-apoapsis/ort-server that referenced this issue Feb 6, 2025
Optional inputs in controlled forms are problematic, because 
(1) Specifying default values for them in the `useForm` hook leads to
    unnecessary payload sent from the form, so rather keep them as
    `undefined`, which means the corresponding parameter is not 
    included in the payload.
(2) Leaving them without default values causes a React warning about a
    component transferring from uncontrolled to controlled state.

Alleviate these problems with a component specifically used for optional 
inputs [1].

[1]: shadcn-ui/ui#410 (comment)
Etsija added a commit to eclipse-apoapsis/ort-server that referenced this issue Feb 6, 2025
Optional inputs in controlled forms are problematic, because 
(1) Specifying default values for them in the `useForm` hook leads to
    unnecessary payload sent from the form, so rather keep them as
    `undefined`, which means the corresponding parameter is not 
    included in the payload.
(2) Leaving them without default values causes a React warning about a
    component transferring from uncontrolled to controlled state.

Alleviate these problems with a component specifically used for optional 
inputs [1].

[1]: shadcn-ui/ui#410 (comment)
Etsija added a commit to eclipse-apoapsis/ort-server that referenced this issue Feb 7, 2025
Optional inputs in controlled forms are problematic, because 
(1) Specifying default values for them in the `useForm` hook leads to
    unnecessary payload sent from the form, so rather keep them as
    `undefined`, which means the corresponding parameter is not 
    included in the payload.
(2) Leaving them without default values causes a React warning about a
    component transferring from uncontrolled to controlled state.

Alleviate these problems with a component specifically used for optional 
inputs [1].

[1]: shadcn-ui/ui#410 (comment)
Etsija added a commit to eclipse-apoapsis/ort-server that referenced this issue Feb 7, 2025
Optional inputs in controlled forms are problematic, because 
(1) Specifying default values for them in the `useForm` hook leads to
    unnecessary payload sent from the form, so rather keep them as
    `undefined`, which means the corresponding parameter is not 
    included in the payload.
(2) Leaving them without default values causes a React warning about a
    component transferring from uncontrolled to controlled state.

Alleviate these problems with a component specifically used for optional 
inputs [1].

[1]: shadcn-ui/ui#410 (comment)
@slythespacecat
Copy link

slythespacecat commented Feb 8, 2025

Here's my solution:

<FormItem className="form-item">
              <FormField
                name="fieldName"
                control={form.control}
                render={({ field }) => {
                  const { formItemId } = useFormField();
                 // get the formItemId
                  return (
                    <>
                      <FormLabel className="form-label">
                        Field Label:
                      </FormLabel>
                      <div className="inner-div">
                       // adding FormControl that i had removed and forgot to put back in
                       <FormControl>
                        <Input
                          // pass id to input
                          id={formItemId} 
                          className="w-full"
                          placeholder="field placeholder"
                          // register value straight from input
                          {...form.register("fieldName")}
                        />
                        </ FormControl>
                        <FormDescription>
                         Field description
                        </FormDescription>
                        <FormMessage />
                      </div>
                    </>
                  );
                }}
              />
            </FormItem>

I tried a bunch of other solutions, but this one was the one that worked best. I didn't change anything in the Form component for this. Some form items are controlled (empty strings), other uncontrolled (the ones that are numbers are undefined) and React doesn't complain. I'm happy (although if this is a bad solution, do let me know....)

Edit: not only that, but the 'Expected number, received nan' messages are also gone. If this is an acceptable way to use the component could be added to the documentation

Edit 2 [clarity]: reason why I wrapped the FormItem around the FormField component was to be able to use the FormItem context to get the field id. the other way around (FormItem inside FormField) i wasn't able to figure out how to pass the field id to the Input field

Etsija added a commit to eclipse-apoapsis/ort-server that referenced this issue Feb 10, 2025
Optional inputs in controlled forms are problematic, because 
(1) Specifying default values for them in the `useForm` hook leads to
    unnecessary payload sent from the form, so rather keep them as
    `undefined`, which means the corresponding parameter is not 
    included in the payload.
(2) Leaving them without default values causes a React warning about a
    component transferring from uncontrolled to controlled state.

Alleviate these problems with a component specifically used for optional 
inputs [1].

[1]: shadcn-ui/ui#410 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests