react-hook-form register type conflict - reactjs

I want to make a custom form input field component with react-hook-form. Here is my code.
// InputField.tsx
interface InputFieldProps {
label: string
register: UseFormRegister<FieldValues>
}
const InputField = ({ label, register }: InputFieldProps) => {
return (
<div>
<label htmlFor="input">{label}</label>
<br></br>
<input
{...(register(label), { required: true })}
id="input"
type="text"
placeholder={`Enter your ${label}...`}
/>
</div>
)
}
// MyForm.tsx
interface IFormValues {
email: string
password: string
}
const MyForm = () => {
const { register, handleSubmit } = useForm<IFormValues>()
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<InputField label="password" register={register} /> // register type error
<button type="submit">Submit</button>
</form>
)
}
But I'm getting this error
Type 'UseFormRegister<IFormValues>' is not assignable to type 'UseFormRegister<FieldValues>'.
Type 'FieldValues' is missing the following properties from type 'IFormValues': email, password
How do I type the register correctly? Or is there better approach to this?

The first error is that you are creating a register from a useForm with a explicit set type IFormValues type, and the expected, as you set in the InputField.tsx, is FieldValues
//Myform.tsx
const { register, handleSubmit } = useForm<IFormValues>()
//InputField.tsx
register: UseFormRegister<FieldValues> //should be the same type as MyForm.tsx
Normally, you don't have to explicity set the type when using useForm. It will acquire the right one by getting the fields you are registering.
And the second one, you are telling your register that every field you are going to register has an email and password. So when you type:
<InputField label="password" register={register} />
Your register was kind of expecting something like:
<InputField label="password" label="email" register={register} />
My opinion: let typescript infer the correct type where it can, and use defaultValues and useFormContext. defaultValues will inter for you the type of register, so you could just copy that type into your InputField.tsx, if you want to keep passing register as a prop. useFormContext will create a register for you from the already opened useForm. I would use the following (I think its a cleaner, simpler approach):
// InputField.tsx
interface InputFieldProps {
label: string
}
const InputField = ({ label }: InputFieldProps) => {
const { register } = useFormContext();
return (
<div>
<label htmlFor="input">{label}</label>
<br></br>
<input
{...(register(label), { required: true })}
id="input"
type="text"
placeholder={`Enter your ${label}...`}
/>
</div>
)
}
// MyForm.tsx
const MyForm = () => {
const { handleSubmit } = useForm({
defaultValues: {email: '', password: ''}
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<InputField label="email"/> //i think this was missing
<InputField label="password"/>
<button type="submit">Submit</button>
</form>
)
}
OBS: if you open a form using useForm, and then another somewhere else in the same page, useFormContext will get lost and throw an error, because it does not know to which form you are referring and it does not allow you to specify it. Be careful with this.

Related

how to use controller component in react-hook-form

I want to validate MuiPhoneInput using react hook form (v7.43.1).
The useForm looks like
const {
register,
handleSubmit,
formState: { errors },
control,
} = useForm();
The controller component looks like
<Controller
name="phone"
control={control}
render={({ field: { onChange, value } }) => (
<MuiPhoneInput
value={value}
onChange={onChange}
defaultCountry={"il"}
variant={"outlined"}
error={!!errors.phone}
helperText={errors?.phone?.message}
/>
)}
rules={{
required: "Phone is required",
}}
/>
But I can't write anything in the input. I tried with other textfields, but nothing works. How to solve this?
You're very close except your MuiPhoneInput is losing reference from the field destructure you're using to get value and onChange. There's an important ref that needs to be set contained inside field. Here's the official example of a Controller component handling a custom masked input.
Here's my working sandbox that demonstrates integrating material-ui-phone-material with react-hook-form.
Since you're setting defaultCountry={"il"} on the MUI component, also set the default value that resolves to inside defaultValues so react-hook-form knows about it.
Adds validation via isNotFilledTel() used inside rules>validate object.
Handling a React.StrictMode issue where ref.focus is undefined.
app.js
import { Controller, useForm } from "react-hook-form";
import MuiPhoneInput from "material-ui-phone-number";
export default function App() {
const {
handleSubmit,
formState: { errors },
control,
} = useForm({
reValidateMode: "onSubmit",
defaultValues: {
phone: "+972",
},
});
const isNotFilledTel = (v: string) => {
return v.length < 15 ? "Phone number is required." : undefined;
};
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="phone"
control={control}
rules={{
required: "Phone is required",
validate: {
inputTelRequired: isNotFilledTel,
},
}}
render={({ field }) => {
return (
<MuiPhoneInput
{...field}
ref={(ref) => {
if (ref && !ref.focus) ref.focus = () => {};
}}
defaultCountry={"il"}
variant={"outlined"}
error={!!errors.phone}
helperText={<>{errors?.phone?.message}</>}
/>
);
}}
/>
{errors.phone && <p>Phone is required.</p>}
<button type="submit">Submit</button>
</form>
);
}

react-hook-form file field TypeScript type

I'm building a contact form with react-hook-form and Typescript. Following official examples here, I've implemented all the fields but the multiple file field.
What type should I use for a multiple-file field?
import * as React from "react";
import { useForm } from "react-hook-form";
type FormData = {
firstName: string;
lastName: string;
files: // <--- this is the type I'm looking for
};
export default function App() {
const { register, setValue, handleSubmit, formState: { errors } } = useForm<FormData>();
const onSubmit = handleSubmit(data => console.log(data));
// firstName and lastName will have correct type
return (
<form onSubmit={onSubmit}>
<label>First Name</label>
<input {...register("firstName")} />
<label>Last Name</label>
<input {...register("lastName")} />
<label>Files</label>
<input type="file" multiple {...register("files")} />
<button
type="button"
onClick={() => {
setValue("lastName", "luo"); // ✅
setValue("firstName", true); // ❌: true is not string
errors.bill; // ❌: property bill does not exist
}}
>
SetValue
</button>
</form>
);
}

How can I set validation rules when all fields are empty React Hook Form

I'm using React Hook Form V7. I have two input fileds and using Controller to control the input.
The below one is what I did now, each field is required.
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
render={({ field }) => <TextField value={field.value} onChange={field.onChange} />}
name="test1"
control={control}
rules={{ required: true }}
/>
<Controller
render={({ field }) => <TextField value={field.value} onChange={field.onChange} />}
name="test2"
control={control}
rules={{ required: true }}
/>
<input type="submit" />
</form>
My question is how can I set instead of each one is required, I want the error message showing when both fields are empty, if only one is empty is accepted.
Is there any onSubmit validation on React Hook Form? Or I need to do the normal validation on the onSubmit function to check the value then set if error message show?
Edit:
this is what I did now:
const [submitError, setSubmitError] = useState(false)
onSubmit((data) => {
const { test1, test2 } = data
if (!test1 && !test2) {
setSubmitError(true)
} else {
setSubmitError(false)
// do submit action
}
})
const errorEmptyMessage = "one of test1 and test2 should has value"
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ field }) => <TextField value={field.value} onChange={field.onChange} />}
name="test1"
control={control}
rules={{ required: true }}
/>
<Controller
render={({ field }) => <TextField value={field.value} onChange={field.onChange} />}
name="test2"
control={control}
rules={{ required: true }}
/>
{submitError && emptyMessage}
<input type="submit" />
</form>
)
I wonder if React Hook Form has a built-in function to do this?
Is there any onSubmit validation on React Hook Form? Or I need to do
the normal validation on the onSubmit function to check the value then
set if error message show?
Yes, You have a nice solution which I recommend it to use nested of normal validation, its name schema validation like YUP, Simply what you need to do is add needed rule, for example (from react-hook-form):
import React from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from '#hookform/resolvers/yup';
import * as yup from "yup";
const schema = yup.object({
firstName: yup.string().required(),
age: yup.number().positive().integer().required(),
}).required();
export default function App() {
const { register, handleSubmit, formState:{ errors } } = useForm({
resolver: yupResolver(schema)
});
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<p>{errors.firstName?.message}</p>
<input {...register("age")} />
<p>{errors.age?.message}</p>
<input type="submit" />
</form>
);
}
If you read the above code, you see you are build a schema for each field needed, and you have a lot of options, for example in your case you may use when to handling on x and y is empty and so on..., also you have a lot of validation schema build as an object like int, min and required, you can check this part.
Also you can do that via onSubmit on normal flow, like this:
const onSubmit = () => {
throw new Error('Something is wrong')
}
handleSubmit(onSubmit).catch((e) => {
// you will need to catch that error
})
and the idea here you check what you need and you can throw the error, exampe:
const { register, handleSubmit } = useForm();
const onSubmit = (data, e) => console.log(data, e);
const onError = (errors, e) => console.log(errors, e);
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<button type="submit">Submit</button>
</form>
);
But from my side, I suggest to use schema validation, its really useful base on my experience with react hook form.
Update 1: More Example:
In above is example how you can build conditions to resolve issue, but simply visit yup and check when,
const schema =
object().shape({
a: string().when(["a", "b"], {
is: (a, b) => !a && !b
then: string().required("At least one is to be selected"),
otherwise: string() // unnecessary
}),
a: string().when(["a", "b"], {
is: (a, b) => !a && !b
then: string().required("At least one is to be selected"),
otherwise: string() // unnecessary
})
});

What are the types of react-hook-form methods?

I am using react-hook-form and I want to have certain parts of the form in an extra component. Therfore I need to pass down some methods. What are the correct typescript types of the methods: register, control, setValue, watch ?
Example Sandbox
interface INameInput {
register: any;
setValue: any;
watch: any;
control: any;
}
const NameInput: React.FC<INameInput> = (props: INameInput) => {
const { register, control, setValue, watch } = props;
return (
<>
<label>First Name</label>
<input name="firstName" ref={register} />
<label>Last Name</label>
<input name="lastName" ref={register} />
</>
);
};
export default function App() {
const { register, control, setValue, watch} = useForm<FormValues>();
return (
<form >
<NameInput
register={register}
control={control}
setValue={setValue}
watch={watch}
/>
</form>
);
}
Here is a page which contains the list of exported Types
https://react-hook-form.com/ts
I think you are after the following type
https://react-hook-form.com/ts#UseFormMethods
eg:
UseFormMethods['register']

React phone input 2 with react hook form

I am using PhoneInput along with react hook form, I want to enable save button only if phone number is valid
Code:
<form onSubmit={handleSubmit(onSubmitRequest)}>
.....................other code..............
<Controller
as={
<PhoneInput
id="pNum"
placeholder="Enter phone number"
className={classes.phoneInput}
inputRef={register({required: true})}
isValid={(inputNumber, onlyCountries) => {
return onlyCountries.some((country) => {
return startsWith(inputNumber, country.dialCode) || startsWith(country.dialCode, inputNumber);
});
}}
/>
}
name="phoneNumber"
control={control}
/>
........................other code...................
<Button
fullWidth
type="submit"
variant="contained"
color={'primary'}
className={classes.submitBtn}
data-testid="customerFormButton"
disabled={!formState.isValid}
>
Save
</Button>
</form>
Here I used PhoneInput as controller along with isValid for it. How can I disable Save button for invalid phone number input?
How are you? I believe that your problem is because you are not configuring the rules for the controller.
You need to change your controller to something like this:
<Controller
as={
<PhoneInput
id="pNum"
placeholder="Enter phone number"
className={classes.phoneInput}
inputRef={register}
isValid={(inputNumber, onlyCountries) => {
return onlyCountries.some((country) => {
return startsWith(inputNumber, country.dialCode) || startsWith(country.dialCode, inputNumber);
});
}}
/>
}
name="phoneNumber"
control={control}
rules= {{required: true}}
/>
ref cannot be currently used on this element. react-phone-input-2.
Until its supported, you can provide a hidden input field which updates its value when the phone updates its value and put the ref on that
Example:
import React, { FC, useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import PhoneInput from 'react-phone-input-2';
import 'react-phone-input-2/lib/style.css';
interface Props {
handleChange: (name: string, val: string) => void;
defaultValue: string;
name: string;
}
const MyComponent: FC<Props> = ({ defaultValue, name, handleChange }) => {
const { register, setValue, watch } = useFormContext(); // Note: needs <FormProvider> in parent for this to be acessible
const nameHidden = `${name}Hidden`;
const handleChangePhone = useCallback(
(val: string) => {
setValue(nameHidden, val, { shouldValidate: true });
handleChange(name, val);
},
[handleChange]
);
return (
<>
<PhoneInput value={defaultValue as string} country="gb" onChange={handleChangePhone} />
<input
type="hidden"
name={nameHidden}
defaultValue={defaultValue}
ref={register({
// validate stuff here...
})}
/>
</>
);
};
export default MyComponent;

Resources