React-hook-form input fields match validation best practice - reactjs

What's the best practice when doing input fields match validation when dealing with React-hook-form? For example, when matching email inputs, etc.
While looking into email match validation with React-hook-form found an issue while trying to separate error messages from "coupled elements" through their validation method. The ref only takes one argument that is used for React-hook-form register, while needing to use useRef to access the current.value for value matching, as follows:
import React, { useRef } from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
function App() {
const { register, handleSubmit, errors } = useForm();
const inputEmail = useRef(null)
const onSubmit = data => {
console.log('onSubmit: ', JSON.stringify(data))
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input
name="email"
type="email"
ref={inputEmail}
/>
{/* desired: show `email` error message */}
<label htmlFor="email">Email confirmation</label>
<input
name="emailConfirmation"
type="email"
ref={register({
validate: {
emailEqual: value => (value === inputEmail.current.value) || 'Email confirmation error!',
}
})}
/>
{errors.emailConfirmation && <p>{errors.emailConfirmation.message}</p>}
<input type="submit" />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
While this pattern seems to be an option when doing input field matching it does not play well with React-hook-form!
For example, the error message is coupled with one input case only and has no separate messages for each independent field, or one of the input fields does not have the register assigned to it, this means that the property required is not set, etc.
So, I'm looking into a good practice or pattern that solves:
Keeping error messages separated by the input field
The validation method, when testing the match should be able to reference the twin field value in a React compliant way and not
through the DOM (document.querySelector, etc)

You shouldn't need the manual ref for inputEmail. Instead, use the getValues method to fetch the current value of your whole form.
const { register, getValues } = useForm()
Then you register both inputs and call getValues from your custom validation.
<input
name="email"
type="email"
ref={register}
/>
<input
name="emailConfirmation"
type="email"
ref={register({
validate: {
emailEqual: value => (value === getValues().email) || 'Email confirmation error!',
}
})}
/>

For this you could use Yup library, which is great:
Add validationSchema to your config object when instantiating useForm and pass a valid Yup schema. Like so:
const Schema = yup.object().shape({
email: yup.string().required('Required field'),
emailConfirmation: yup
.string()
.oneOf([yup.ref('email')], 'Emails must match')
.required('Required field'),
});
// How to add it to your useForm
const { register } = useForm({
validationSchema: Schema
})
Your component should look something like this:
function App() {
const { register, handleSubmit, errors } = useForm({
validationSchema: Schema
});
const onSubmit = data => {
console.log('onSubmit: ', JSON.stringify(data))
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input
name="email"
type="email"
ref={register}
/>
{/* desired: show `email` error message */}
<label htmlFor="email">Email confirmation</label>
<input
name="emailConfirmation"
type="email"
ref={register}
/>
{errors.emailConfirmation && <p>{errors.emailConfirmation.message}</p>}
<input type="submit" />
</form>
);
}

Related

Problem with using if statement in react-hook-form

Validation in react-hook-form using if, else statement return only when return value is true. But if the statement is false it returns nothing.
import * as React from "react";
import * as ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import "./styles.css";
const App = () => {
const {
register,
handleSubmit,
getValues,
formState: { isValid }
} = useForm({
mode: "onChange",
defaultValues: {
firstName: "",
lastName: ""
}
});
const onSubmit = () => {
if (isValid) {
console.log("valid");
} else {
console.log("invalid");
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>First Name</label>
<input type="text" {...register("firstName", { required: true })} />
<label>Last Name</label>
<input type="text" {...register("lastName", { required: true })} />
<input type="submit" />
</form>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I don't know what's wrong. Can someone help please.
Here's the code from codesandbox
I don't think anything is wrong with the code. If you only enter the last name and hit submit, it highlights the first name box. And same for the opposite. If you only enter the first name and hit submit, it highlights the second box. This seems to me like a default behavior from the react hook form. Here you can see I added a useEffect to it to print out the validity. When the form is valid you can see it shows correctly.
This seems to be a feature from the react hook form to prevent submitting if it is not valid.
Edit: Look into their import { ErrorMessage } from "#hookform/error-message"; system they have in place. And Here is an updated code sandbox with error handling
Your form wont submit unless required fields are entered on inputs:
const onSubmit = () => {
if (!isValid) {
console.log("valid");
} else {
console.log("invalid");
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>First Name</label>
<input type="text" {...register("firstName")} />
<label>Last Name</label>
<input type="text" {...register("lastName")} />
<input type="submit" />
</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
})
});

Simple react form validation with yup

I'm trying to learn form validation with yup and react-hook-form and, ideally want to set the validation to also require at least one of the two different-named radio options, but first understand why the isValid variable is false.
Here's my code:
import React from "react";
import { useForm } from "react-hook-form";
import * as yup from "yup"
const App = () => {
const { register, handleSubmit, watch, errors } = useForm();
const onSubmit = async data => {
console.log('form data', data);
const isValid = await schema.isValid();
console.log('isValid', isValid);
}
return (
<>
{console.log('errors', errors)}
<form onSubmit={handleSubmit(onSubmit)}>
<input name="name" defaultValue="test" ref={register} />
<input name="nameTwo" ref={register({ required: true })} />
{errors.exampleRequired && <span>This field is required</span>}
<label>
<input
name="test"
type="radio"
ref={register}
value="test"
/>
</label>
<label>
<input
name="testTwo"
type="radio"
ref={register}
value="testTwo"
/>
</label>
<input type="submit" />
</form>
</>
);
}
const schema = yup.object().shape({
name: yup.string(),
nameTwo: yup.string().required(),
test: yup.string(),
testTwo: yup.string()
})
export default App
So the schema defines only one field (nameTwo) as required, but even when filling in that field it still is false. Why is isValid not true? And how is it best to go about making either one of the two radio buttons (test or testTwo) required?
Stackblitz demo: https://stackblitz.com/edit/react-93vahd
Thank you
The isValid property is available in the formState object returned from the useForm hook.
You'll notice that onSubmit is not called unless the Yup validation passes. Therefore isValid will always be true when checking inside the onSubmit callback.
import React from 'react';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
const App = () => {
const {
register,
handleSubmit,
errors,
formState: { isValid }
} = useForm();
const onSubmit = async data => {
console.log('form data', data);
};
return (
<>
{console.log('isValid', isValid)}
{console.log('errors', errors)}
<form onSubmit={handleSubmit(onSubmit)}>
<input name="name" defaultValue="test" ref={register} />
<input name="nameTwo" ref={register({ required: true })} />
{errors.exampleRequired && <span>This field is required</span>}
<label>
<input name="test" type="radio" ref={register} value="test" />
</label>
<label>
<input name="testTwo" type="radio" ref={register} value="testTwo" />
</label>
<input type="submit" />
</form>
</>
);
};
const schema = yup.object().shape({
name: yup.string(),
nameTwo: yup.string().required(),
test: yup.string(),
testTwo: yup.string()
});
export default App;

Conditional validation with react hook form

Here is my form looks like and also CodeSanbox. currently I'm using react-hook-form
as you can see form has 3 inputs. Submit button should be disabled until all the required fields are entered.
Two use case:
If "Check" is unchecked:
only "id" should be validated and submit button should get enabled. "firt" and "last" names should not be part of form data
If "Check" is checked
all the fields should be validated
first and last names are only required if "Check" is checked. so its not checked then form should only validate "ID" field. if "Check" is checked then all fields should get validated.
problem I'm having is if I enter id, form state is still "invalid". Form is expecting to enter values for first and last name.
I would appreciate any help.
I have updated your CodeSanBox code and also adding the full code here:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import "./index.css";
function App() {
const {
register,
handleSubmit,
errors,
formState,
unregister,
setValue,
getValues,
reset
} = useForm({
mode: "onBlur",
reValidateMode: "onBlur",
shouldUnregister: true
});
//console.log(formState.isValid);
console.log(errors);
const [disabled, setDisabled] = useState(true);
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
useEffect(() => {
// #ts-ignore
if (disabled) {
console.log("unregister");
reset({ ...getValues(), firstName: undefined, lastName: undefined });
unregister(["firstName", "lastName"]);
} else {
console.log("register");
register("firstName", { required: true });
register("lastName", { required: true });
}
}, [disabled]);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="id">ID</label>
<input
name="id"
placeholder="id"
ref={register({ required: true, maxLength: 50 })}
/>
{errors.id && <p>"ID is required"</p>}
<fieldset disabled={disabled}>
<legend>
<input
type="checkbox"
name={"name"}
ref={register}
onClick={() => setDisabled(!disabled)}
/>
<span>Check</span>
</legend>
<label htmlFor="firstName">First Name</label>
<input
name="firstName"
placeholder="Bill"
onChange={(e) => {
console.log(e.target.value);
setValue("firstName", e.target.value);
}}
ref={register({ required: !disabled })}
/>
{errors.firstName && <p>"First name is required"</p>}
<label htmlFor="lastName">Last Name</label>
<input
name="lastName"
placeholder="Luo"
onChange={(e) => setValue("lastName", e.target.value)}
ref={register({ required: !disabled })}
/>
{errors.lastName && <p>"Last name is required"</p>}
</fieldset>
<input type="submit" disabled={!formState.isValid} />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
First I found that you set disabled state as false which should be true as an initial value, and regarding the issue, I have used reset and getValues functions when the disabled state changes.
EDIT for you to recognize code changes easy, I have restored all the code at CodeSanBox.
This whole validation behavior (UX) is definitely making things a bit harder, however, there are a couple of things that you should leverage from the library such as:
watch
validate
getValues
import React from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import "./index.css";
function App() {
const {
register,
handleSubmit,
errors,
formState: { isValid, touched },
getValues,
trigger,
watch
} = useForm({
mode: "onBlur"
});
const onSubmit = (data) => {
alert(JSON.stringify(data));
};
const validate = (value) => {
if (getValues("name")) { // read the checkbox value
return !!value;
}
return true;
};
const isChecked = watch("name"); // watch if the name is checked
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="id">ID</label>
<input
name="id"
placeholder="id"
ref={register({ required: true, maxLength: 50 })}
/>
{errors.id && <p>"ID is required"</p>}
<fieldset disabled={!isChecked}>
<legend>
<input
type="checkbox"
name={"name"}
ref={register}
onChange={() => trigger()} // you want update isValid due to state change, and also those extra two inputs become required
/>
<span>Check</span>
</legend>
<label htmlFor="firstName">First Name</label>
<input
name="firstName"
placeholder="Bill"
ref={register({
validate
})}
/>
// make sure input is touched before fire an error message to the user
{errors.firstName && touched["firstName"] && (
<p>"First name is required"</p>
)}
<label htmlFor="lastName">Last Name</label>
<input
name="lastName"
placeholder="Luo"
ref={register({
validate
})}
/>
{errors.lastName && touched["lastName"] && (
<p>"Last name is required"</p>
)}
</fieldset>
<input type="submit" disabled={!isValid} />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
CSB:
https://codesandbox.io/s/react-hook-form-conditional-fields-forked-n0jig?file=/src/index.js:0-1831
on your ref, dont use hard coded bool true, ref={register({ required: true})}, but your dynamic ref={register({ required: disabled })}
do notice that because your mode: "onBlur" config, the button won't be abled until id field blurred
You just need to replace true .from ref: required:true..... Instead use const 'disabled' ....in input of first and last name .
So as to achieve dynamic change

DefaultValues of react-hook-form is not setting the values to the Input fields in React JS

I want to provide default values in the input field using react-hook-form. First I retrieve the user data from the API endpoint and then setting the state users to that user data. Then I pass the state users to the defaultValues of useForm().
import React, { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import axios from "axios";
function LoginFile() {
const [users, setUsers] = useState(null);
useEffect(() => {
axios
.get("http://localhost:4000/users/1")
.then((res) => setUsers(res.data));
}, []);
useEffect(() => {
console.log(users);
}, [users]);
const { register, handleSubmit, errors } = useForm({
defaultValues: users,
});
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
Email <input type="email" name="email" ref={register} /><br />
firstname <input name="firstname" ref={register} /><br/>
<input type="submit" />
</form>
</div>
);
}
export default LoginFile;
I did by the above code but didn't work as expected. All the input fields are still empty. I want to have some default values in the input field of my form.
The problem is that during the first render, users is the useState hook's initial value, which is null. The value only changes after the axios.get() request finishes, which is after the initial render. This means that the the default values passed to useForm is null.
The documentation for defaultValues says the following:
defaultValues are cached on the first render within the custom hook. If you want to reset the defaultValues, you should use the reset api.
So, you'll just need to use reset to reset the form manually to the values which you fetch. The documentation for reset says the following:
You will need to pass defaultValues to useForm in order to reset the Controller components' value.
However, it's unclear from the documentation whether null is enough as the defaultValues, or if you need to pass it a proper object with fields for each input. To play it safe, let's assume it's the latter.
The code for doing this would look something like this:
function LoginFile() {
const [users, setUsers] = useState({ email: "", firstname: "" });
const { register, handleSubmit, errors, reset } = useForm({
defaultValues: users,
});
useEffect(() => {
axios.get("http://localhost:4000/users/1").then((res) => {
setUsers(res.data);
reset(res.data);
});
}, [reset]);
useEffect(() => {
console.log(users);
}, [users]);
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
Email <input type="email" name="email" ref={register} />
<br />
firstname <input name="firstname" ref={register} />
<br />
<input type="submit" />
</form>
</div>
);
}
Additionally, if the only reason for the useState hook is to store the value for defaultValues, you don't need it at all and can clean up the code to be:
function LoginFile() {
const { register, handleSubmit, errors, reset } = useForm({
defaultValues: { email: "", firstname: "" },
});
useEffect(() => {
axios.get("http://localhost:4000/users/1").then((res) => {
reset(res.data);
});
}, [reset]);
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
Email <input type="email" name="email" ref={register} />
<br />
firstname <input name="firstname" ref={register} />
<br />
<input type="submit" />
</form>
</div>
);
}
That definitely Worked. Using the reset API and the UseEffect.
Starting with empty stings as default values and the updating them as effects with the reset. Here is my code. Was using TypeScript with Ionic here as well...
const { control: editControl, handleSubmit: editSubmit, reset } = useForm<EditFormType>({
defaultValues: {
question: "",
optionA: "",
optionB: "",
optionC: "",
optionD: "",
}
});
useEffect(() => {
let defaults ={
question: editQuestion?.body,
optionA: editQuestion?.options[0].body,
optionB: editQuestion?.options[1].body,
optionC: editQuestion?.options[2].body,
optionD: editQuestion?.options[3].body,
}
reset(defaults)
}, [editQuestion, reset])

Resources