(Part 1) Build quality forms with React ๐Ÿš€

(Part 1) Build quality forms with React ๐Ÿš€

Optimize developer experience, ship with confidence

ยท

6 min read

Photo by Kelly Sikkema

When it comes to building forms with React, there are tons of possibilities out there in the React Ecosystem. Many libraries have emerged to answer specific needs, and it can be a little bit overwhelming at first: which one to choose? what strategy to adopt?

Even without using a library, just using pure React, you can basically go two ways: controlled or uncontrolled. We will briefly discuss this, so if you're new to React, you might want to start there. Then, I will show you how I like to build my forms today: using react-hook-form, yup and TypeScript. Those tools allow me to quickly build any type of form with a great Developer Experience and confidence about their robustness and stability.

Ready to become a Form Master? Let's go ๐Ÿš€

Controlled VS Uncontrolled

To start with the basics, I wanted to show you the difference between what we call Controlled forms and Uncontrolled forms. To understand this, you just need to know one thing: when an input value is controlled by React using a state value and a change handler, then it's a controlled input.

This means an uncontrolled input works just like a form element outside of React: when the user inputs data into the field, the updated information is reflected without React needing to do anything.

Nothing like some code to get a good grasp of it:

Controlled Form

function ControlledForm() {
    // We maintain state values for our inputs
  const [username, setUsername] = React.useState('')
  const [password, setPassword] = React.useState('')

  function onSubmit(event) {
        // During the submit phase, we simply need to access
        // the input values from our component's state
    event.preventDefault()
    console.log(username, password)
  }

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="username">Username</label>
      {/**
        * Remember, we need a value and an onChange handler
        * for the input to be considered controlled
        **/}
      <input
        id="username"
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />

      <label htmlFor="password">Password</label>
      <input
        id="password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />

      <button type="submit">Login</button>
    </form>
  )
}

Uncontrolled Form

function UncontrolledForm() {
    // In the onSubmit handler, we get the form values
    // from the event itself (input are referenced by their name)
  function onSubmit(event) {
    event.preventDefault()
    const { username, password } = event.target
    console.log(username.value, password.value)
  }

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="username">Username</label>
      <input id="username" name="username" type="text" />

      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password" />

      <button type="submit">Login</button>
    </form>
  )
}

Additional notes

When using a controlled input, its state value should always be initialized with a value (like an empty string for text input). If you initialize it to null or undefined (React.useState() will initialize the state value to undefined), React will consider your input uncontrolled. And because you update the state value with a change handler as soon as the user starts typing something in the input, you will get this warning:

Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components

So, do as our friend React says, and decide between controlled and uncontrolled, but do not mix both ๐Ÿ˜‡

react-hook-form + yup + TypeScript = โค๏ธ

Since I started using React, I have tried many libraries and tools to build the forms I was working on. Today, I ALWAYS use react-hook-form and yup, because I think those two libraries are amazing and work really well together. They abstract a lot of things that can become quite tedious and repetitive over time, and they give me all the control I need to build performant, flexible, and extensible forms for pretty much all use cases.

Because I now always work with TypeScript, I will also show you the benefit of using it when building forms. If you're not familiar with TypeScript, don't worry, there won't be too much of it and it should be easy to understand. That being said, I strongly recommend you start learning it, I promise it will change your life as a web developer!

Here is a great book to start learning TypeScript: https://typescript-book.com/

What is react-hook-form?

This library leverage the power of hooks to gain full control over uncontrolled inputs. It's really easy to use and takes a small amount of code, which is one of their main goals:

Reducing the amount of code that you have to write is one of the primary goals for React Hook Form.

It's also a tiny library without any dependencies, packed with optimizations to minimize the number of re-renders and fasten component mounting.

It works like this (code sample from their documentation):

import React from "react";
import { useForm, SubmitHandler } from "react-hook-form";

type Inputs = {
  example: string,
  exampleRequired: string,
};

export default function App() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>();

  const onSubmit: SubmitHandler<Inputs> = data => console.log(data);

  console.log(watch("example")) // watch input value by passing the name of it

  return (
    /* "handleSubmit" will validate your inputs before invoking "onSubmit" */
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* register your input into the hook by invoking the "register" function */}
      <input defaultValue="test" {...register("example")} />

      {/* include validation with required or other standard HTML validation rules */}
      <input {...register("exampleRequired", { required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}

      <input type="submit" />
    </form>
  );
}

That's it! With a few lines of code, you get a functional, type-safe form with basic validation. You just need to register your inputs using the register function, wrap your submit handler in the handleSubmit for validation, render any errors that might have occurred during previous validations. To do so, you get all those utilities back from the useForm call.

There is a lot more that you can do with this library, but in order to keep this post concise I will encourage you to visit their official documentation if you want to learn more about it: https://react-hook-form.com/get-started

โš ๏ธ Spoiler alert: there will be a real-life example at the end, where I'll show you how to build a login and a registration form.

What is Yup?

Yup is a Javascript object schema validator: it lets you define a schema to describe how a valid object should look like, and allows you to validate an object using this schema. If you know Joi, Yup is heavily inspired by it, except it relies on client-side validation as its primary use-case.

According to their documentation:

Yup is a JavaScript schema builder for value parsing and validation. Define a schema, transform a value to match, validate the shape of an existing value, or both. Yup schemas are extremely expressive and allow modeling complex, interdependent validations, or value transformations.

Here is basic example of how it works:

// First, you define an object schema for your validation
let schema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required().positive().integer(),
  email: yup.string().email(),
  website: yup.string().url(),
  createdOn: yup.date().default(function () {
    return new Date();
  }),
});

// check validity of an object
schema
  .isValid({
    name: 'jimmy',
    age: 24,
  })
  .then(function (valid) {
    valid; // => true
  });

If you want to learn more, check their docs. What I personally love about Yup is the readability of it, and how verbose the schema is. For example, in the schema above, you can literally read out loud "ok, age is a number, is required, and must be a positive integer". That's great!

Now it's time to see how react-hook-forms and yup work side by side. Check out part 2 of this article to see how this is done, along with practical examples: a login form, and a registration form.

ย