DEV Community

rrd
rrd

Posted on

How I've used react-hook-form library

Disclaimer: Sorry for all possible typos and potentially confusing info. I just wanted to show my approach for implementing forms using React, without spending too much time

For one of my projects, I had to implement a simple, but a relatively long form ( 40 fields ). In this post I'm going to show you the approach that I've taken.

Requirements
My form has to be simple, but flexible. It has various input fields. A lot of different validation requirements.

Technologies
I've decided not to reinvent the wheel and used standard tech:

  • react-hook-form ( because it's easy to extend on your native form)
  • yup for validation ( because some validation on my project is tricky)

Like in many other projects in my standard setup, I'm using

  • eslint with airbnb styleguide
  • prettier for code formatting

All code is written using react/typescript.

Approach
What I end up doing is to develop a custom input components.
I can use this components anywhere ( deeply nested ) anywhere on my form.

// components/form/TextInput.tsx
// example of text input component, I've similar for other inputs
import React from 'react';
import { ErrorMessage } from '@hookform/error-message';
import { UseFormReturn } from 'react-hook-form';
import { CustomInputField } from 'utils/types';
import classnames from 'classnames';
import ConnectForm from './ConnectForm';
import ErrorPrompt from './ErrorPrompt';

export const TextInput = ({
  name,
  label,
  required,
  ...rest
}: CustomInputField & React.HTMLProps<HTMLInputElement>) => (
  <ConnectForm>
    {({ register, formState: { errors } }: UseFormReturn) => (
      <div className="mb-3 row">
        <label htmlFor={`text-field-${name}`} className="form-label col-sm-2">
          {label}
          {required && <span className="required"> * </span>}
        </label>
        <div className="col-sm-10">
          <input
            id={`text-field-${name}`}
            {...register(name)}
            {...rest}
            className={classnames('form-control', { 'is-invalid': errors[name] })}
          />
          <ErrorMessage errors={errors} name={name} render={ErrorPrompt} />
        </div>
      </div>
    )}
  </ConnectForm>
);

export default TextInput;
Enter fullscreen mode Exit fullscreen mode

ConnectForm component is designed as per react-hook-form documentation
https://react-hook-form.com/advanced-usage/#ConnectForm.

So my final form structure is very simple:

  const methods = useForm({
    resolver: yupResolver(FormValidationSchema),
    mode: 'onSubmit',
    reValidateMode: 'onChange',
  });

  return (
    <div className="registration-form container-sm">
      <h1>Registration Form</h1>
      <FormProvider {...methods}>
        <form
          onSubmit={methods.handleSubmit(onSubmit)}
          className="row g-3 needs-validation"
          noValidate
        >
          <fieldset>
            <legend>User Details:</legend>
            <TextInput label="Given name" name="givenName" placeholder="e.g. Jane" required />
            <TextInput label="Family name" name="surname" placeholder="e.g. Doe" required />
            <SingleDateInput label="Date of birth" name="dateOfBirth" />
            <RadioInput
              label="Gender"
              name="gender"
              options={['Male', 'Female', 'Another gender', 'Unknown']}
              required
            />

Enter fullscreen mode Exit fullscreen mode

Validation

I validate my form using validation resolver and validation schema, which I setup in a separate file

// form.tsx
  const methods = useForm({
    resolver: yupResolver(FormValidationSchema),
    mode: 'onSubmit',
    reValidateMode: 'onChange',
  });
Enter fullscreen mode Exit fullscreen mode
// validationSchema.ts
export const FormValidationSchema = yup
  .object({
    givenName: yup
      .string()
      .required(VALIDATION_MESSAGE_REQUIRED)
      .max(30, VALIDATION_MESSAGE_MAX_CHAR),
    surname: yup
      .string()
      .required(VALIDATION_MESSAGE_REQUIRED)
      .max(30, VALIDATION_MESSAGE_MAX_CHAR),
    dateOfBirth: yup
      .date()
      .transform(parseDateString)
      .min(subYears(today, 140), 'Date of Birth can\'t be more than 140 years in the past') // eslint-disable-line
      .max(today),
Enter fullscreen mode Exit fullscreen mode

Unit Tests
I've also developed it using TDD approach, so I've written tests first and have a good coverage.

describe('Registration Form', () => {
  test('renders correctly', async () => {
    const { findByText } = render(<RegistrationForm />);

    expect(await findByText(/User Details/)).toBeTruthy();
  });

  test('has all the fields', async () => {
    const { findByText } = render(<RegistrationForm />);

    expect(await findByText(/User Details/)).toBeTruthy();
    expect(screen.getByText('Given name')).toBeInTheDocument();
    expect(screen.getByText('Family name')).toBeInTheDocument();
    expect(screen.getByText('Date of birth')).toBeInTheDocument();
  });

  test.skip('validation works', async () => {
    render(<RegistrationForm />);
    userEvent.click(await screen.findByText('Submit'));

    await wait();

    expect(screen.getAllByText(VALIDATION_MESSAGE_REQUIRED).length).toBe(3);
  });

Enter fullscreen mode Exit fullscreen mode

Conclusion
In my view final product is clear and can be picked up by any other developer without too much learning. Flexible html allows it to structure in any way when this form will get a custom design from an another developer ( CSS expert )

I hope this content was useful for some people.
I cut corners on certain implementation details, but let me know if you want me to elaborate on certain stuff.

Happy to answer any questions.

Discussion (0)