Managing React Forms Efficiently at Scale

prasanjit

ਪ੍ਰਸੰਨਜੀਤ | Prasanjit

Posted on July 15, 2020

Managing React Forms Efficiently at Scale

Managing forms in React can easily become painful as your app scales. It could probably be due to the unorganised approach you followed while starting a project or writing the same boilerplate in every component or even sometimes applying hacks for tricky use cases. Speaking of the solutions, there are many form libraries out there and some of them handle forms really well but organising and keeping the code tidy is still up to you. In this article we’re going to solve this problem by building our form as simple as coding forms in vanilla HTML which can be used elegantly in any production app.

To give you an idea, we’ll make this simple code below render our form. We will use React Hook Form, a performant and easy to plug-in library for React forms.

<Form onSubmit={handleSubmit}>
  <Input name="name" label="Name" />
  <Input name="email" label="Email" type="email" />
  <RadioGroup name="gender" options={radioOptions} />
  <Dropdown name="location" options={dropdownOptions} />
  <Checkbox name="terms" label="Accept terms of service" />
  <Button>Submit</Button>
</Form>
Enter fullscreen mode Exit fullscreen mode

Before we jump into the code, first I’d like you to know the goals that we are trying to accomplish through this approach.

  • Cutting down the boilerplate - we should be able to render the forms with clean and DRY code
  • Validations - Perform simple and complex validations easily
  • Flexibility - we should be able to put input fields at any level nested inside the form
  • Accessibility - At the very basic level, the form elements should be easily accessible via keyboard
  • Self detected submit button state - the button should automatically enable/disable or show loader as per the form state
  • Performance - Last but not the least, it is crucial one especially when rendering large number of fields inside the form.

Set up React app

With the above requirements in mind, let’s begin with setting up our react app by running npx create-react-app react-form && yarn add react-hook-form

Next, we will create our reusable form and input components to cut down all the boilerplate.
React Hook Form provides us useForm and useFormContext hooks to get the form context right away and within the nested components respectively. We’ll be using both of these hooks for the communication between the form and input components.
First we’ll create the Form component followed by the input components such text fields, checkboxes, radio buttons, etc.

Build the Form component

We’ll initialise the form using useForm hook, and pass on all the methods as props to the form via FormProvider component. This will provide form state to the input components.

import React from 'react'
import { useForm, FormProvider } from 'react-hook-form'

export const Form = ({ initialValues, children, onSubmit }) => {
  const methods = useForm({ defaultValues: initialValues })

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        {children}
      </form>
    </FormProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Build the form elements

Now that we have our Form component ready, we’ll create the form elements starting with the Input component.
Since we don’t know how deeply we would nest the input components, we’ll use useFormContext to hook it up with its parent Form component that we created before.

import React from 'react'
import { useFormContext } from 'react-hook-form'

export const Input = ({ label, name }) => {
  const { register } = useFormContext()

  return (
     <label>
       {label}
       <input name={name} ref={register} />
     </label>
  )
}
Enter fullscreen mode Exit fullscreen mode

Note: Using useFormContext with FormContext might affect performance by a bit due to re-renders but it should be negligible if you’re not doing any expensive computation within these components. If so, you can use memo to compare the dirty state of the form.

Checkbox Component

We’ll create and hook this component the same way we did in the Input component, just adding type checkbox to it.

import React from 'react'
import { useFormContext } from 'react-hook-form'

export const Checkbox = ({ label, name }) => {
  const { register } = useFormContext()

  return (
    <label>
      <input type="checkbox" name={name} ref={register} />
      {label}
    </label>
  )
}
Enter fullscreen mode Exit fullscreen mode

Radio Buttons Component

Since there’s no use case for a single radio button on a web page, we’ll create a RadioGroup component which will accept an array of fields and render a group of radio button

import React from 'react'
import { useFormContext } from 'react-hook-form'

export const RadioGroup = ({ name, label, options }) => {
  const { register } = useFormContext()

  return (
    <div>
      <div>{label}</div>
      {options && options.map(option => (
        <label key={option.value}>
          <input
            type="radio"
            name={name}
            value={option.value}
            ref={register}
          />
          {option.label}
        </label>
      ))}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Dropdown Component

This will be slightly different from the previous components as we’ll be using a 3rd party plugin for this. Keeping accessibility in mind, the best one I found is downshift by Kent C Dodds. Another good thing about this library is that it only provides functionality for the dropdown and let us code our own UI.
Let’s install the plugin using yarn add downshift and create the component as below:

import React, { useEffect } from 'react'
import { useFormContext } from 'react-hook-form'
import { useSelect } from 'downshift'

export const Dropdown = ({
  name,
  options,
  label,
  initialValue,
  placeholder = 'Select...'
}) => {
  const { register, setValue, getValues } = useFormContext()

  const findInitialItem = () => {
    const defaultValue = initialValue || getValues()[name]
    if (defaultValue) {
      return options.find(o => o.value === defaultValue)
    }
  }

  const {
    isOpen,
    selectedItem,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    getItemProps,
  } = useSelect({
    items: options,
    initialSelectedItem: findInitialItem()
  })

  useEffect(() => {
    if (selectedItem) {
      setValue(name, selectedItem.value);
    }
  }, [selectedItem, name, setValue]);

  return (
    <div>
      <button type="button" {...getToggleButtonProps()}>
        <label {...getLabelProps()}>{label}</label>
        <input type="hidden" name={name} ref={register} />
        <div>
          {selectedItem ? selectedItem.text : placeholder}
        </div>
      </button>
      <div {...getMenuProps()}>
        {isOpen && (
          options.map((item, index) => (
            <div key={`${item.value}${index}`} {...getItemProps({ item, index })}>
              {item.text}
            </div>
          ))
        )}
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Button Component

The purpose of building the Button component is to make it handle enabled/disabled/loading state itself. Here, we’re simply disabling the button until the form fields are dirty or when the form is being submitted.

import React from 'react'
import { useFormContext } from 'react-hook-form'

export const Button = ({ children }) => {
  const { formState: { isDirty, isSubmitting } } = useFormContext()

  return (
    <button type="submit" disabled={!isDirty || isSubmitting}>
      {children}
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

Validating the form

Till now, we have made our form functional and able to submit form data.
As our next requirement, we need to add validations to our form elements. We’ll allow two types of validations: one is to simply check if a required field is filled or not and other by providing a pattern to validate the value.
As an example, the Input component that we created before will now receive two extra props: required and validation.

import React from 'react'
import { useFormContext } from 'react-hook-form'

export const Input = ({ label, name, required, validation }) => {
  const { register, errors } = useFormContext()

  return (
    <div>
      <label>
        {label}
        <input
        name={name}
        ref={register(validation || { required: !!required })}
      />
      </label>
     {errors[name] && <i>{errors[name].message}</i>}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The same way we can implement validation in our other components.

Summing up

In this article, we’ve created the components with bare minimum code. If you wish to play around the code, here’s the CodeSandbox link.

You can also find ready to use TypeScript version of this code (demo here) on GitHub.

💖 💪 🙅 🚩

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related