Allow empty string as defaultValue but reject on validation with Zod, react-hook-form and material UI
Asked Answered
M

3

6

I'm using material ui components with react-hook-form and zod validation. I have a bloodType select field:

const bloodTypes = [ "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-" ] as const;
const schema = z.object({
  bloodType: z.enum(bloodTypes),
});

type FormType = z.infer<typeof schema>;

The problem comes in the mui component in the defaultValue prop. Since this is a controlled component I need to give it a defaultValue but Typescript complains that the "" value is not assignable to the bloodType type.

<FormControl fullWidth error={!!errors.bloodType} margin="normal">
  <InputLabel id="bloodtype-label">Tipo de sangre</InputLabel>
  <Controller
    name="bloodType"
    defaultValue=""  // <-------- HERE
    control={control}
    render={({ field }) => (
      <Select
        {...field}
        labelId="bloodtype-label"
        label="Tipo de sangre"
      >
        {bloodTypes.map((bloodtype) => (
          <MenuItem key={bloodtype} value={bloodtype}>
            {bloodtype}
          </MenuItem>
        ))}
      </Select>
    )}
  />
  <FormHelperText>{errors.bloodType?.message}</FormHelperText>
</FormControl>

That makes sense since bloodType only allows the values defined in the schema, but I would like the initial value to be empty.

I tried setting the defaultValue to undefined but then MUI throws an alert MUI: You have provided an out-of-range value 'undefined' for the select. So my question is how can I allow an empty string as defaultValue but reject on zod validation and maintain correct types?

As a workaround I'm using .refine to check if the value is "" and return false so it fails the validation, but I feel that it is not the correct approach.

Moonstone answered 25/5, 2023 at 4:20 Comment(1)
Have you found a clean solution? It's so annoying they assumes that a form's values must always be valid even from its initial state.Pejsach
V
0

The way I have found to resolve this is by casting the empty string as the field type.

<Controller
    name="bloodType"
    defaultValue={"" as FormType["bloodType"]}  // <-------- HERE
    control={control}
    render={({ field }) => (
      <Select
        {...field}
        labelId="bloodtype-label"
        label="Tipo de sangre"
      >
        {bloodTypes.map((bloodtype) => (
          <MenuItem key={bloodtype} value={bloodtype}>
            {bloodtype}
          </MenuItem>
        ))}
      </Select>
    )}
  />

This also works within the useForm hook, if you setup your defaultValues there as well.

const formResults = useForm<FormType>({
    resolver: zodResolver(schema),
    defaultValues: {
      bloodType: '' as FormType['bloodType'],
    },
  });

I think this calms the TS compiler in a way that allows the schema to stay clean without added refine statements or other validation, and with the least amount of "lying" about the types and values involved.

Valer answered 12/2, 2024 at 17:24 Comment(2)
I've tried this but it says cannot find name 'FormType'Elwina
FormType is just a placeholder for the type inferred from the validation schema. In the original post it is type FormType = z.infer<typeof schema>;. You should fill this in with whatever you named your type.Valer
C
-1

Yeah encountering the same problem for likely forever now. I guess right now the only way of getting through this is refine OR method i created but it's not perfect. Create a nullable typescript type
export type Nullable<T> = { [K in keyof T]: T[K] | null };
Then use it in useForm like this
useForm<Nullable<ShippingManage>>
And now you can provide null into defaultvalues BUT it's becoming edgy when you need more manipulations and kinda complex so it's a way to go for smaller forms. I hope we'll find something better to solve this problem...

Cassondracassoulet answered 1/8, 2023 at 11:50 Comment(0)
G
-2

Here's how you can have an initial empty string as the default value for the bloodType select field while still maintaining correct types and validating against the Zod schema.

State to manage the default value for bloodType

const [defaultBloodType, setDefaultBloodType] = useState('');

Custom validation function for defaultValue

const validateDefaultBloodType = (value: string) => {
    // Perform Zod validation
    try {
      schema.parse({ bloodType: value as FormType['bloodType'] });
    } catch (error) {
      // Handle validation error, e.g., show error message
      console.error('Validation Error:', error.message);
    }
    // Set the default value for bloodType state
    setDefaultBloodType(value);
  };

  // Use useEffect to call the validation function when the component mounts
  useEffect(() => {
    validateDefaultBloodType('');
  }, []); // Empty dependency array ensures it runs once on mount
Guillemette answered 5/8, 2023 at 0:24 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.