Intro
Have you ever started a brand-new React project and thought, “Which form library should I use?” Or maybe you are in an existing projecting thinking, “There has to be a better way to do this.” The freedom within React to choose your own tools is a great feature, but it can also be easy to get decision paralysis from all the options. Starting from basic forms and working into advanced forms, this article will compare and contrast different methods and libraries to find the easiest way to create forms that are typed, reusable, and simple.
React Uncontrolled Forms
Reading from the React documentation, there is a recommended way to write forms using standard HTML that already feels cumbersome. Uncontrolled forms are the first method. They do not have any event listeners and get the values when the form is submitted.
const UncontrolledRegisterForm = () => { const usernameInput = React.createRef(); const passwordInput = React.createRef(); const handleSubmit = (event) => { const value = { username: usernameInput.current.value, password: passwordInput.current.value, }; event.preventDefault(); console.log(value); } return ( <form onSubmit={handleSubmit}> <label> Username: <input name="username" type="text" ref={usernameInput} /> </label> <label> password: <input name="password" type="text" ref={passwordInput} /> </label> <input type="submit" value="Submit" /> </form> ); }
There is a username and password field, both initialized in the constructor with a React ref, and a binding for on submit. Tedious Boilerplate, no typing, and not easy to reuse.
React Controlled Forms
Moving through the React documentation, we see controlled forms. Very similar to Uncontrolled forms, the only difference is using on change events to get live updates to the form values.
const ControlledRegisterForm = () => { const [form, setForm] = useState({ userName: '', password: '', }); const handleChange = (event) => { const { name, value } = event.target; switch (event.target.name) { case 'username': setForm((state) => ({ ...state, userName: value, })); return; case 'password': setForm((state) => ({ ...state, password: value, })); return; } }; const handleSubmit = (event) => { event.preventDefault(); console.log(form); }; return ( <form onSubmit={handleSubmit}> <label> Username: <input name="username" type="text" value={form.userName} onChange={handleChange} /> </label> <label> password: <input name="password" type="text" value={form.password} onChange={handleChange} /> </label> <input type="submit" value="Submit" /> </form> ); };
Now I have a component state, and can see changes to individual form controls – but man – That’s a lot of boilerplate.
It is time for me to find a form library to make this easier
Formik
The first library I will share is Formik. Formik is a good choice because it takes a lot of the boilerplate out of the equation. You may be wondering why I skipped Redux-Form. It is a largely agreed upon community standard that form state is local and should not be held in a higher scope of state like Redux – and Redux-Form affirms this claim.
Basic Formik Form
Let’s start with the basics. Using Formik requires setting up the initial values as an object, writing a submit function, and using the Formik component along with the Formik form field components. In this example, I will be using a React Functional Component.
const initialValues = { username: '', password: '', }; const submitForm = (values, setSubmitting) => { setTimeout(() => { console.log(values); setSubmitting(false); }, 400); }; const FormikRegisterForm = () => ( <div> <Formik initialValues={initialValues} onSubmit={(values, { setSubmitting }) => submitForm(values, setSubmitting) } > {({ isSubmitting }) => ( <Form> <Field name="username" type="text" /> <Field name="password" type="password" /> <button type="submit" disabled={isSubmitting}> Submit </button> </Form> )} </Formik> </div> );
Things are looking a little better, it’s definitely more readable than React Controlled Forms. There is a little less boilerplate. Let’s add some validation to this and see what that looks like. To do that, I will create a validate function that returns an object of errors, pass it to the Formik component as an argument, and then use the Error Message component to display the errors.
const validate = (values) => { const errors = {}; if (!values.username) { errors.username = 'invalid username'; } if (!values.password) { errors.password = 'invalid password'; } return errors; }; const FormikRegisterForm = () => ( <div> <Formik initialValues={initialValues} validate={(values) => validate(values)} onSubmit={(values, { setSubmitting }) => submitForm(values, setSubmitting) } > {({ isSubmitting }) => ( <Form> <Field name="username" type="text" /> <ErrorMessage name="username" /> <Field name="password" type="password" /> <ErrorMessage name="password" /> <button type="submit" disabled={isSubmitting}> Submit </button> </Form> )} </Formik> </div> );
A little verbose on validation, but we can clean that up later.
Angular CLI: Common Use Cases
The most common use case is for general application development. The CLI has more than enough tools for most project. It’s also great for learning Angular as the majority of tutorials will assume you are using the CLI.
If you making a library for Angular, such as a component framework, the CLI allows for side by side development of the library and a demo / sandbox application for testing.
Formik Hook useFormik()
Another way to use Formik is to use their hook. The hook is not necessarily better or less work, it is just a different pattern that conforms with using hooks to handle logic and component state in functional components.
const validate = (values) => { const errors = {}; if (!values.username) { errors.username = 'invalid username'; } if (!values.password) { errors.password = 'invalid password'; } return errors; }; const RegisterFormFormikHook = () => { const formik = useFormik({ initialValues: { username: '', password: '', }, onSubmit: (values) => { console.log(values); }, validate: validate, }); return ( <form onSubmit={formik.handleSubmit}> <label htmlFor="username">username</label> <input id="username" name="username" type="username" onChange={formik.handleChange} value={formik.values.username} onBlur={formik.handleBlur} /> {formik.touched.username && formik.errors.username ? ( <div>{formik.errors.username}</div> ) : null}= <label htmlFor="password">password</label> <input id="password" name="password" type="password" onChange={formik.handleChange} value={formik.values.password} onBlur={formik.handleBlur} /> {formik.touched.password && formik.errors.password ? ( <div>{formik.errors.password}</div> ) : null} <button type="submit">Submit</button> </form> ); };
The useFormik hook makes it easier to use standard HTML form elements or integrate with a third party UI library that is not Formik.
React Hook Form
React Hook Form is my personal preference and recommendation. It has the best developer experience of all the other forms, and contains more features than the other libraries.
Performance
React Hook Form isolates re-renders and has finite control over subscriptions on changes. This makes it much more performant than controlled forms and also gives more finite control over rendering when compared to formik. The team has also done some speed tests and have results of faster mounting times highlighted on the [website](scroll down a bit to their render count example).
UX
Built into the form is a smooth and very clean user experience, along with a customizable UI that can be themed. It’s incredibly easy to use, the visuals are very pleasant, and they make forms look incredibly professional.
Usability
Leveraging hooks, React Hook Form is lightweight and requires very few lines of code. Simple built-in patterns make recall a breeze, and I can say this is the part I most enjoy. There are also many built-in validations, but it is still easy to add custom validation as well. Creating a large, complex form becomes a trivial task when paired with YUP or another validation schema library.
On top of its ease of use, React Hook Form also offers a form builder tool. The tool allows creating a form with an easy-to-use UI, and generates the code needed. This can also be very useful for passing off this kind of work to a UX designer who may not be as familiar with form code, and allows them to predictably integrate form UI into their designs.
Basic Form
Let’s start with a simple form that has some validations and plain logging for error and submit actions. And full transparency—I generated this form on React Hook Form’s website using their form tool. It took about 1 minute to complete.
export default function GuestForm() { const { register, handleSubmit, formState: { errors }, } = useForm(); const onSubmit = (data) => console.log('values', data); console.log('Errors', errors); return ( <form onSubmit={handleSubmit(onSubmit)}> Guests (max 4): <input type="number" placeholder="Number of Guests" {...register('Number of Guests', { required: true, max: 4, min: 1 })} /> <label>Pets: </label> <input type="checkbox" placeholder="Pets" defaultValue="false" {...register('Pets')} /> <select defaultValue="Sammich" {...register('Lunch Meal Selection', { required: true })} > <option value="Sammich">Sammich</option> <option value="Steak"> Steak</option> <option value="Ribs"> Ribs</option> <option value="Tacos"> Tacos</option> </select> <input type="submit" /> </form> ); }
The key concept around using React Hook Form, is the idea of ‘registering’ your form controls to the hook. This is done but passing the form control name to the register function and spreading the returned object. This must be done for all form controls.
Schemas
Schemas make forms easier to define, as well as improve typing and reuse. A schema is just an object that can be passed to define the form and rules, while making sure the results are predictable. Let’s start by using a typescript schema.
// The form and validation is defined by the schema type AboutMeFormData = favoriteColor: string; age: String; } const AboutMeForm = () => { const { register, handleSubmit, formState: { errors } } = useForm<AboutMeFormData>(); const onSubmit = data => console.log(data); console.log('Errors', errors); return ( <form onSubmit={handleSubmit(onSubmit)}> favorite color <input {...register("favoriteColor")} /> <p>{errors.favoriteColor?.message}</p> age <input {...register("age")} /> <p>{errors.age?.message}</p> <input type="submit" /> </form> ); }
Intellisense can now pick up the form typing, and the form will validate against the typescript type. This is a good start, but if you want more fine-tuned or custom validation, a schema library is the way to go. Yup is one of the most common schema libraries, but you can use whichever you like. In the next example, Yup is used to define the schema types as well as the validation. Notice how the age requires a positive integer in this example.
// The form and validation is defined by the schema const schema = yup.object({ favoriteColor: yup.string().required(), age: yup.number().positive().integer().required(), }).required(); const AboutMeForm = () => { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: yupResolver(schema) }); const onSubmit = data => console.log(data); console.log('Errors', errors); return ( <form onSubmit={handleSubmit(onSubmit)}> favorite color <input {...register("favoriteColor")} /> <p>{errors.favoriteColor?.message}</p> age <input {...register("age")} /> <p>{errors.age?.message}</p> <input type="submit" /> </form> ); }
Wrap Up
React has a lot to offer when it comes to 3rd party form integration, but it can be challenging to decide on which one. Testing out these different libraries should give some insight into developer experience, but my personal recommendation is React Hook Form or secondly Formik.