Autofill is an essential feature for web forms. It saves time and minimizes errors by automatically filling in previously entered data. However, certain things might not work when you use autofill in your forms, such as validation. In this article, I will provide a way to handle autofill using MUI, React Hook Form, and Yup, and to have it work even with autofill.
Background
At Trailhead, we have researched many different form libraries. We were inspired by Angular Reactive Forms (despite some of its drawbacks) and sought to find a similar package for React. React Hook Form turned out to be the closest to Angular Reactive Forms and, in some ways, even better. It is lightweight, flexible, and offers a simple and intuitive API.
One of its advantages is its validations resolver, which opens the way to validation libraries like Yup, kneel before Zod, Vest, and many others. Among these choices, we ultimately chose Yup because it offers a declarative API, supports schema validation, and has a wide range of validation methods for various data types, which helps build easy-to-read and maintainable validation schemas.
Below, I will show you how we overcame one key issue we encountered while integrating React Hook Form with Yup for validation and MUI as our UI component library. The issue was with autofill.
Autofill Issue with React Hook Form
One common issue with form autofill, especially in Chrome, is that form libraries, like React Hook Form, can only catch auto-filled values once the user interacts with the form, page, or console. Browsers do not propagate any events when auto-filling a form for security reasons. As a result, form libraries may be unable to validate or handle the auto-filled values properly.
Let’s Fix Autofill
First of all, we need to create the LoginForm component, which uses the useForm
hook to set up the form with default values, and the TextField
component from MUI for input fields.
import { TextField, Box } from '@mui/material';
import { useForm } from 'react-hook-form';
interface ICredentialsForm {
username: string;
password: string;
}
function LoginForm() {
const {
register,
} = useForm<ICredentialsForm>({
defaultValues: {
username: '',
password: '',
},
});
return (
<Box display="flex" flexDirection="column" gap="20px" maxWidth="200px">
<TextField
label="Username"
variant="outlined"
type="text"
{...register('username')}
/>
<TextField
label="Password"
variant="outlined"
type="password"
{...register('password')}
/>
</Box>
);
}
export default LoginForm;
Next, we need to add validations and the Login
button. Since we decided to use the Yup library, let’s create a validation schema first.
import { object, string } from 'yup';
export const validationSchema = object().shape({
username: string()
.default('')
.required(),
password: string()
.default('')
.required(),
});
Now, we can add a resolver to the useForm
hook and use formState for the Login
button.
import { Button, TextField, Box } from '@mui/material';
import { useCallback } from 'react';
import { yupResolver } from '@hookform/resolvers/yup';
import { validationSchema } from './validation-schema';
function LoginForm() {
const {
register,
formState: { isValid },
} = useForm<ICredentialsForm>({
resolver: yupResolver<ICredentialsForm>(validationSchema),
defaultValues: {
username: '',
password: '',
},
});
const login = useCallback(() => {
// Login code
}, []);
return (
<Box display="flex" flexDirection="column" gap="20px" maxWidth="200px">
// ...
<Button disabled={!isValid} variant="contained" onClick={login}>
Login
</Button>
</Box>
);
}
export default LoginForm;
Here’s a result we’ve get when we run this:
Notice, however, that the Login
button is disabled because the browser expects user interactions to propagate the onChange event, which will update the form values. If we try to click on the disabled button, the browser will catch this interaction and trigger the onChange event first, and the form will get values just before the onClick event is triggered. While this works, it can be confusing for the user.
Let’s see what we can do to catch autofill and enable the Login
button. There’s a way to detect autofill animation on the MUI Text Field. To do this, we must add the onAnimationStart
property to the InputProps in TextField and catch even.animationName equals mui-auto-fill
.
import { useCallback, AnimationEvent } from 'react';
function LoginForm() {
//...
const onUserNameAnimationStart = useCallback(
() =>
(event: AnimationEvent<HTMLDivElement>): void => {
if (event.animationName === 'mui-auto-fill') {
}
},
[]
);
//...
return (
<Box display="flex" flexDirection="column" gap="20px" maxWidth="200px">
<TextField
label="Username"
variant="outlined"
type="text"
InputProps={{
onAnimationStart: onUserNameAnimationStart(),
}}
{...register('username')}
/>
//...
</Box>
);
}
export default LoginForm;
We can do this only for the username because browsers allow us to keep the username empty but not the password.
Next, we need to add the extra form value autofilled
, set it to true
when the mui-auto-fill
animation happens, and adjust the yup schema.
interface ICredentialsForm {
username: string;
password: string;
autofilled: boolean;
}
const validationSchema = object().shape({
username: string()
.default('')
.when('autofilled', ([autofilled], schema) => {
return autofilled ? schema.notRequired() : schema.required('Required');
}),
password: string()
.default('')
.when('autofilled', ([autofilled], schema) => {
return autofilled ? schema.notRequired() : schema.required('Required');
}),
autofilled: boolean().default(false),
});
function LoginForm() {
const {
register,
setValue,
formState: { isValid },
} = useForm<ICredentialsForm>({
resolver: yupResolver<ICredentialsForm>(validationSchema),
defaultValues: {
username: '',
password: '',
autofilled: false,
},
});
const onUserNameAnimationStart = useCallback(
() =>
(event: AnimationEvent<HTMLDivElement>): void => {
if (event.animationName === 'mui-auto-fill') {
setValue('autofilled', true, { shouldValidate: true });
}
},
[]
);
//...
return (
//...
);
}
export default LoginForm;
In the code above, we’ve used the when
method from yup to make the username
and password
properties not required if autofilled
equals true
.
So, here’s the final result. As you can see, the Login
button is enabled when the form is autofilled.
Bonus
To not let autofilled
values break our validations, we need to clear it after any user interaction. To do so, we need to check if any field becomes dirty and set the autofilled
property back to false
:
import { useCallback, useEffect, AnimationEvent } from 'react';
//...
function LoginForm() {
const {
register,
setValue,
formState: { isValid, dirtyFields },
watch,
} = useForm<ICredentialsForm>({
//...
});
const formValues = watch();
//...
useEffect(() => {
if (Object.keys(dirtyFields).length > 0 && formValues.autofilled) {
setValue('autofilled', false, { shouldValidate: true });
}
}, [formValues, dirtyFields, setValue]);
return (
//...
);
}
export default LoginForm;
Conclusion
As you can see, handling autofill using React Hook Form, Yup, and MUI may be challenging, but investing just a little time into dealing with these edge cases can ultimately enhance the user experience for your application.
As always, Trailhead is available to help. You can contact us if you’d like help implementing this approach in your own React forms.