Build a Production-ready Form with React

ยท

5 min read

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.

wecorder.gif

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>
...

meocording.gif

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>
  );
ย