Build a Production-ready Form with React
Hey ๐ค, and welcome to join me to create a production-ready form.
Most times when we build react forms from courses or random projects online, they are not optimized enough. Since they intend to get us up and running quickly, well it is just fine and they can't be blamed (Oops you dare not).
But going forward in one's career, there is the need to write better code.
The most obvious penalty with forms that are not well optimized is the performance problem. In that when an input is changed, it causes the whole form to re-render. In fact, memoization won't save us. One way out of this mess could be to place the state for each input in its respective components. Even so, it just doesn't do enough.
The objective here is to help you build a production-ready form that is just fine. No performance itch and everyone is OK. So join me ๐บ.
For this project, I will be enhancing an already existing form that was created in my previous article.
To help write this so-called production-ready form, I will be using a react form library: the popular ones are formik, react-final-form, and react-hook-form.
And sorry to cut you in, react-hook-form is what we'll be using. Well, not for any particular reason except that it is the one I am more familiar with. One of these days, I would be having an article on the alternatives but for now all hail react-hook-form ๐คซ.
Enough of the talking and let's code. (โ'โก'โ)
Install the
react-hook-form
package and that's all really to get started.$: npm install react-hook-form
Let's see what our form ๐ looks like: Note react-hook-form can only work on functional components.
function Form (){
return <form>
...
</form>
}
export default Form
OMG ๐ !! Sorry to bother you again,
But I'll be assuming you are a react developer, and thus I'm making a lot of assumptions regarding your JS and react knowledge except writing a production-ready form.
Whiiiipy!!!, back to our code
Step 1: Import the useForm hook from react-hook-form
...there the magic begins.
import { useForm } from "react-hook-form"
Step2: register each input to react-hook-form
Formerly, my code looked like this ๐
... function Form (){ ... return <form onSubmit={this.save}> <div className="columns is-mobile is-centered"> <div className="column is-one-third"> <div className="field"> <label className="label">Product Name: </label> <input className="input" type="text" name="name" value={name} onChange={this.handleChange} required /> </div> ...
Our emphasis is on the input field. The obvious thing you would notice is the input fields get smaller. The register function has handled the stress with value, name, and the onChange attributes.
... function Form (){ const { register, handleSubmit } = useForm(); return <form onSubmit={handleSubmit ((data => save(data))}> ... <input className="input" type="text" {...register('name')} /> </div> ...
You must have replaced every input field in the form this way except the submit button.
Step 3: Before now, some fields were required so let's get that back in.
<input
className="input"
type="text"
{...register('name', {required: true})} // ๐
/>
Step 5: return errors to screen when there are.
If an input field has an error we simply return an error message with a logic like this
errors && 'error message'
...
function Form (){
const { register, handleSubmit, formState: {errors} } = useForm();
return <form onSubmit={handleSubmit ((data => save(data))}>
...
<input
className="input"
type="text"
{...register('name', {required: true})}
/>
{errors.name && "name is required"}
</div>
...
But doing this will cause screen flicks when the error pops in the screen. Trust me when I say this is a bad experience for a user.
To remedy this, we would have to create an error component to better handle the flicking.
export function Error({ error }) {
return <div className={"error"}>{error ? error.message : " "}</div>;
}
And then replace how we rendered error before now.
... function Form (){ const { register, handleSubmit, formState: {errors} } = useForm(); return <form onSubmit={handleSubmit ((data => save(data))}> ... <input className="input" type="text" {...register('name', { required: true })} /> <Error errors={errors.name} /> </div> ...
After implementing the previous instruction, you might start to wonder why no error pops up on the screen.
Well, do not panic. This cause is error.message props is empty. To fill it up read the next step.
Here comes the help โฐ(ยฐโฝยฐ)โฏ: Don't panic, replace the Boolean passed in as arguments into our validators with a string. Yeepy!!
...
<input
className="input"
type="text"
{...register('name', { required: "name field is required" })}
/>
<Error errors={errors.name} />
</div>
...
And that's it. No flickring on the screen. Nice and clean LOL.
Further Reading
The react-hook-form is brilliant. It doesn't come with any perf itch because any change to input doesn't cause the whole form re-rendering.
However, it is awesome to note some input requires more validation other than ensuring they are required. For example
...{required: true, maxLength: 23, min: 2, pattern: /^[A-Za-z]+$/i}
To ensure our input fields do not get messy with validation codes, we can put all validations in a single repository thanks to yup.
npm install @hookform/resolvers yup
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from "yup";
...
const schema = yup.object({
username: yup.string().required(),
password: yup.string().matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/, {message: "pasword is too weak}).required(),
}).required();
export default function App() {
const { register, handleSubmit, formState:{ errors } } = useForm({
resolver: yupResolver(schema)
});
...
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<p>{errors.username?.message}</p>
<input {...register("password")} />
<p>{errors.password?.message}</p>
<input type="submit" />
</form>
);