Form validation in React, as simple as it gets
Ioannis Potouridis
Posted on April 14, 2020
There are a lot of form or object schema validation libraries, such as react-hook-form
, formik
, yup
to name a few. In this example we are not going to use any of them.
To start with, we are going to need a state to keep our values. Let's say the following interface describes our values' state.
interface Values {
firstName: string;
password: string;
passwordConfirm: string;
}
And our form component looks like this.
const initialValues: Values = {
firstName: '',
password: '',
passwordConfirm: '',
}
function Form() {
const [values, setValues] = useState<Values>(initialValues);
const handleChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
setValues((prev) => ({ ...prev, [target.name]: target.value }));
};
return (
<form>
<label htmlFor="firstName">First name</label>
<input
id="firstName"
name="firstName"
onChange={handleChange}
type="text"
value={values.firstName}
/>
<label htmlFor="password">Password</label>
<input
id="password"
name="password"
onChange={handleChange}
type="password"
value={values.password}
/>
<label htmlFor="passwordConfirm">Confirm password</label>
<input
id="passwordConfirm"
name="passwordConfirm"
onChange={handleChange}
type="password"
value={values.passwordConfirm}
/>
</form>
)
}
All we need is an errors object that is calculated based on our current values' state.
const errors = useMemo(() => {
const draft: { [P in keyof Values]?: string } = {};
if (!values.firstName) {
draft.firstName = 'firstName is required';
}
if (!values.password) {
draft.password = 'password is required';
}
if (!values.passwordConfirm) {
draft.passwordConfirm = 'passwordConfirm is required';
}
if (values.password) {
if (values.password.length < 8) {
draft.password = 'password must be at least 8 characters';
}
if (values.passwordConfirm !== values.password) {
draft.passwordConfirm = 'passwordConfirm must match password';
}
}
return draft;
}, [values]);
Then, you'd modify your JSX in order to display the error messages like so.
<label htmlFor="firstName">First name</label>
<input
aria-describedby={
errors.firstName ? 'firstName-error-message' : undefined
}
aria-invalid={!!errors.firstName}
id="firstName"
name="firstName"
onChange={handleChange}
type="text"
value={values.firstName}
/>
{errors.firstName && (
<span id="firstName-error-message">{errors.firstName}</span>
)}
Now the messages appear when we first see the form but that's not the best use experience that we can provide. In order to avoid that there are two ways:
- Display each error after a user interacted with an input
- Display the errors after user submitted the form
With the first approach we would need a touched
state, where we keep the fields that the user touched or to put it otherwise, when a field loses its focus.
const [touched, setTouched] = useState<{ [P in keyof Values]?: true }>({});
const handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) => {
setTouched((prev) => ({ ...prev, [target.name]: true }));
};
And our field would look like this.
<label htmlFor="firstName">First name</label>
<input
aria-describedby={
touched.firstName && errors.firstName
? 'firstName-error-message'
: undefined
}
aria-invalid={!!touched.firstName && !!errors.firstName}
id="firstName"
name="firstName"
onBlur={handleBlur}
onChange={handleChange}
type="text"
value={values.firstName}
/>
{touched.firstName && errors.firstName && (
<span id="firstName-error-message">{errors.firstName}</span>
)}
In a similar way, we would keep a submitted
state and set it to true
when a user submitted the form for the first time and update our conditions accordingly.
And, that's it!
It may be missing a thing or two, and may require you writing the handlers and the if
statements for calculating the errors, but it's solid solution and a good start to validate forms in React.
Posted on April 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 19, 2024